The page and the service worker were strict TypeScript; the tests, the build, the home page's generator, the load tests, boot.js and the directory's site.js were plain JavaScript that nothing checked. All of it is TypeScript now, in five programs, each with its own globals, all checked by npm run typecheck: the page (tsconfig.json, with boot.ts and the directory's site.ts), the worker, Node's (tsconfig.tools.json: the Node tests, the browser suite, the build, the site generator, the load harness), and k6's (tsconfig.k6.json, with @types/k6). Node 24 runs the .ts files by stripping their types, so they hold only TypeScript that erases (erasableSyntaxOnly) and import each other by .ts name; package.json says "type": "module" for it, and Dockerfile.web moves to Node 24 to run build.mts. k6 2.3 strips its scripts' types itself. The browser suite's page.evaluate calls reach the page's globals; the page's tsc run now also writes its declarations to target/page-types, which the suite's program includes, so those calls are typed by the page's own code rather than by hand-written copies. boot.ts is compiled by build.mts with swc, as the worker is, and named by the hash of what is served. The directory's build has no Node, so site.js is compiled from site.ts and committed (node web/build.mts --directory-script), and tests/directory-script.ts fails if they drift. Checked: the site generator's 139 pages are byte for byte what the JavaScript made; the page build's app.js is identical and only boot.js (now minified) and its hash differ; contrast, page-smoke, page-unit, native-bridge, the browser suite (96 passed, 2 skipped) and the directory's and ipx's Rust tests pass; k6 parses each load script. The load tests themselves did not run: they build a release binary outside the capped builder. .agents/skills/security-audit is an installed skill's, not ours, and is left as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
2.4 KiB
TypeScript
49 lines
2.4 KiB
TypeScript
import http from 'k6/http';
|
|
import { check, sleep } from 'k6';
|
|
import { BASE, FEEDS, ITEMS, as, me, pick, feedId, p95, body, type Feed, type ListPage } from './lib.ts';
|
|
|
|
// An evening's browsing: twenty-five people at once opening their feed list, All Subscriptions,
|
|
// a feed, a search, the Directory and a feed's page in it. Each answer's budget is well above what
|
|
// it takes now, so it fails on a query that has started asking once per feed -- the Directory
|
|
// asked the database three questions a feed until 0.10.0 -- not on a slow minute. Fifty measured
|
|
// the queue for what was SQLite's one connection (#136) more than any query.
|
|
export const options = {
|
|
scenarios: {
|
|
evening: {
|
|
executor: 'ramping-vus',
|
|
stages: [{ duration: '15s', target: 25 }, { duration: '45s', target: 25 }, { duration: '10s', target: 0 }],
|
|
},
|
|
},
|
|
thresholds: {
|
|
http_req_failed: ['rate==0'],
|
|
checks: ['rate==1'],
|
|
// About twice each one's p95 on 2026-10-05 on Tower, and no less than 50ms, where a few ms
|
|
// either way is noise. SQLite: 89, 17, 10, 22, 112, 126ms; Postgres: 54, 34, 24, 44, 71, 88ms.
|
|
'http_req_duration{name:feeds}': p95(200, 150),
|
|
'http_req_duration{name:all-entries}': p95(50, 100),
|
|
'http_req_duration{name:feed-entries}': p95(50, 100),
|
|
'http_req_duration{name:search}': p95(60, 100),
|
|
},
|
|
};
|
|
|
|
export default function () {
|
|
const u = me();
|
|
const feeds = http.get(`${BASE}/api/feeds`, as(u, 'feeds'));
|
|
check(feeds, { 'your thirty feeds': r => r.status === 200 && body<Feed[]>(r).length === 30 });
|
|
|
|
const all = http.get(`${BASE}/api/entries?limit=50&filter=all&sort=published&dir=desc`, as(u, 'all-entries'));
|
|
check(all, { 'All Subscriptions, a page of it': r => r.status === 200 && body<ListPage>(r).entries.length === 50 });
|
|
|
|
const f = pick(body<Feed[]>(feeds));
|
|
const one = http.get(`${BASE}/api/feeds/${f.id}/entries?limit=50&sort=title&dir=asc`, as(u, 'feed-entries'));
|
|
check(one, { 'a feed of yours, every item': r => r.status === 200 && body<ListPage>(r).entries.length === ITEMS });
|
|
|
|
// Every generated item mentions the weather, in its text, not its title.
|
|
const found = http.get(`${BASE}/api/entries?q=weather&limit=50`, as(u, 'search'));
|
|
check(found, { 'a search of everything you read': r => r.status === 200 && body<ListPage>(r).total === 30 * ITEMS });
|
|
|
|
// Not the Directory: since #204 it is ipx-directory's, which this harness does not start (#245).
|
|
|
|
sleep(1 + Math.random() * 2);
|
|
}
|