Skip to content

Commit f2e7083

Browse files
test(runtime,client): pay the reader-contexts pin's first loads at module scope and share one auth server across the rotated-token cases (#26227)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XP5yokEzFQq2X7EXPg3PnHb)_ Part of #23181 (slice 13, `domain:server` seat 1). The card stays open for its other rows and for the refresh that judges these two. Clause-②: no Metadata-protocol: none Test-only diff: two test files, nothing published, so `skip-changeset` rides the create. ## The reading this slice used The newest refresh run, not a landed refresh PR (none has landed since the card's table): `shard-timings-refresh` run `40275747937` (schedule, 2026-10-26T05:43Z, head `2a728ba2a`, 14 hourly full-battery runs `40200279306`..`40272142430`), whose `--diff` step logs every test file over budget. Of this slice's six named files it still lists two: | file | before (CI, slowest run in window) | |:--|--:| | `@objectstack/runtime` `src/stored-metadata-reader-contexts.pin.test.ts` (local) | 16.31 s (`40200279306`) | | `@objectstack/client` `src/auth-rotated-session-token.test.ts` | 13.62 s (`40200279306`) | No longer listed (earlier slices closed them): runtime `declared-capability-absence-warn-once.test.ts` and `stored-metadata-body-boundary.pin.test.ts`, rest `import-template-route.test.ts`, client `auth-login-register-envelope.test.ts`. Not touched. ## What changed **`runtime/src/stored-metadata-reader-contexts.pin.test.ts`.** The file already booted once per file. What its `beforeAll` paid was first use: measured in one process, the first boot took 1962 ms and a second identical boot 673 ms, because the boot `import()`s better-auth and the services' entries lazily and runs every path cold. The boot is now one function (`bootKernel`), and a module-scope `warmUpFirstLoads()` runs it once on a throwaway app (its own manifest, object and database), signs the dev administrator in and runs one sandboxed body, then stops it before `beforeAll`. That is the repo's convention (AGENTS.md: clocked windows measure behaviour, never loading), the same shape as `@objectstack/verify`'s `boot-warm-up.testkit.ts` and rest's `import-template-route.test.ts`. A failed warm-up throws during collection, so it cannot pass silently. `NODE_ENV` is put back as found. **`client/src/auth-rotated-session-token.test.ts`.** Every case built a fresh engine, schema sync and `AuthManager` (about 150 ms each, measured) before signing a user up. The cases now share one engine and one `AuthManager` (`fileServer()`, built by the first case, destroyed in `afterAll`). Each case still signs up a user of its own through a client of its own, and every assertion is about that user's credential. Because the default audience posture is `invite_only`, each user enters through a pending `sys_invitation` row, the lane `@objectstack/verify`'s signUp and the runtime pin above take. The module-scope warm-up keeps its own server and destroys it before the first case, as before; case ④'s pins (one module-scope `await signedIn();`, one `before*` hook, no widened clock) hold unchanged. ## Measurements (local, 4 vCPU, shared box, through `os-verify-lock`) Module-line milliseconds (the number the timing table reads), a copy of each file at base `601c77384` interleaved with the file at `453c7e3ae` (this diff before the merge with `origin/main`), three runs each: | file | before | after | median | |:--|:--|:--|:--| | runtime pin | 5126 / 4957 / 4922 | 3712 / 3750 / 4008 | 4957 → 3750 (−24 %) | | client rotated-token | 3874 / 4561 / 4161 | 3085 / 3116 / 3371 | 4161 → 3116 (−25 %) | - **Isolation:** `--sequence.shuffle` with seeds 101, 202 and 303 on both files: 27/27 and 8/8 pass every run, and the verbose order confirms the cases ran shuffled (seed 101 runs client case ③ first). - **Assertions unchanged:** the `it`/`describe` title lines hash equal at base `601c77384` and head (runtime 28 lines `451ebc4925fbeaaf`, client 12 lines `36683f1bc93a4451`); `expect(` counts 50 = 50 and 43 = 43. No timeout, budget, project or roster entry moved; nothing deleted. - **What it costs.** The client change removes work (seven fewer engine and auth-manager builds per run). The runtime change moves first-load cost out of the clocked window and adds one warm boot (about 0.7 s of CPU during collection): the whole vitest run's wall is unchanged (33.1 / 33.6 s before, 33.3 / 32.9 s after, two runs on a loaded box). ## Does it close the rows? Not proven. On the before ratio of each file (CI 16.31 s against 4957 ms local, 13.62 s against 4161 ms), the projections are about 12.3 s for the runtime pin and 10.2 s for the client file. The next landed refresh decides; this PR claims no row as closed. What is left is behaviour: - runtime pin: about 40 sandboxed body runs, each of which instantiates a fresh QuickJS WASM module by design (`sandbox/quickjs-runner.ts`, ADR-0102 D2), with a major GC roughly every third run (single runs of the same action measured 28 to 142 ms). The `/meta`-authored actions bind on the first poll (measured: 1 poll each), so no wait is left to cut. - client file: the real scrypt derivations (each case's sign-up plus its password checks) at production parameters. ## Acceptance notes - Other runtime rows in the same reading, outside this slice's file surface and untouched: `src/domains/mcp-package-object-discovery.test.ts` (local) 14.93 s and `src/domains/meta-list-projection-parity.test.ts` (repo) 13.60 s, both run `40200279306`. - `packages/runtime` typecheck needs `@objectstack/driver-turso` built (`organization-ownership-boot-guard.turso-remote.kernel.test.ts` imports it) even though it is outside `@objectstack/runtime^...`; a closure-only build reports TS2307 there. Local setup, not a defect. ## Verification Run at head `df1b49998` (the branch merged with `origin/main`; the two test files are unchanged by the merge), all through `os-verify-lock` except the gates and eslint: - `pnpm --filter @objectstack/runtime test`: Test Files 455 passed (455), Tests 7408 passed | 19 skipped (7427). - `pnpm --filter @objectstack/client test`: Test Files 52 passed (52), Tests 671 passed (671). - `pnpm --filter @objectstack/runtime typecheck` and `pnpm --filter @objectstack/client typecheck`: exit 0, `check:test-typecheck: OK` for both; each changed file is in its package's `tsconfig.test.json` program (`tsc --listFilesOnly`, 1 hit each). - `eslint --no-inline-config packages/runtime packages/client`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 56 commands; each ran with its exit code captured before any pipe, all 56 exit 0. `--ran`: "Run reconciliation — 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. - `--sequence.shuffle` (seeds 101, 202, 303) re-run on this head: 27/27 and 8/8 every run. No ablation: no tested behaviour changed. The diff changes how the two files arrange their servers, not what any case asserts. --- _Generated by [Claude Code](https://claude.ai/code/session_017YG7VcjjNVTjKQp5HhS3TU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f42b55f commit f2e7083

2 files changed

Lines changed: 126 additions & 23 deletions

File tree

‎packages/client/src/auth-rotated-session-token.test.ts‎

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,16 @@
7575
// reds it, the lever is that case's own work, ⛔ not a timeout.
7676
//
7777
// ⛔ It shares nothing a case asserts on. Its engine and manager are its own
78-
// and are destroyed before any case starts. Every case still builds a fresh
79-
// engine, a fresh `AuthManager` and a fresh sign-up. What it leaves warm is
80-
// process-level: the module registry, sql.js's compiled WASM and the JIT,
81-
// which the first case used to leave to every later case.
78+
// and are destroyed before any case starts. The cases then share one engine
79+
// and one `AuthManager` of their own (`fileServer()`), and every case still
80+
// signs up a fresh user. What the warm-up leaves warm is process-level: the
81+
// module registry, sql.js's compiled WASM and the JIT, which the first case
82+
// used to leave to every later case.
8283
//
8384
// ⛔ Do not move it into a hook, and do not answer a recurrence by raising a
8485
// timeout: vitest clocks a hook exactly as it clocks a test body.
8586

86-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
87+
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
8788
import { readFileSync } from 'node:fs';
8889
import { fileURLToPath } from 'node:url';
8990
import { createHmac } from 'node:crypto';
@@ -200,16 +201,34 @@ const newClient = (manager: AuthManager, token?: string): ObjectStackClient =>
200201
manager.handleRequest(new Request(String(input), init)),
201202
});
202203

203-
/** A real `AuthManager` plus a client whose socket IS that manager. */
204-
const arrange = async () => {
204+
/** A real `AuthManager` over a fresh engine. */
205+
const arrangeServer = async () => {
205206
const engine = await makeEngine();
206207
const manager = new AuthManager({
207208
secret: SECRET,
208209
baseUrl: ORIGIN,
209210
dataEngine: engine,
210211
plugins: { twoFactor: true },
211212
} as never);
213+
return { engine, manager };
214+
};
212215

216+
/**
217+
* [#23181] The file's ONE server, built by the first case that signs in and
218+
* shared by every later one. Each case still signs a user of its own up on it,
219+
* through a client of its own, and every assertion here is about that user's
220+
* credential: a rotation revokes only the rotating user's sessions, and a
221+
* case reads its own user by address. So no case reads another case's writes,
222+
* and the order of cases does not matter (measured with `--sequence.shuffle`).
223+
* What a case no longer pays is a fresh engine, a schema sync and a fresh
224+
* better-auth instance of its own, about 150 ms each, measured.
225+
*/
226+
let server: ReturnType<typeof arrangeServer> | undefined;
227+
const fileServer = () => (server ??= arrangeServer());
228+
229+
/** A real `AuthManager` plus a client whose socket IS that manager. */
230+
const arrange = async () => {
231+
const { engine, manager } = await fileServer();
213232
return { engine, manager, client: newClient(manager) };
214233
};
215234

@@ -220,6 +239,22 @@ const nextEmail = () => `rotation-${++emailSeq}-${Date.now()}@example.com`;
220239
const signedIn = async () => {
221240
const { engine, manager, client } = await arrange();
222241
const email = nextEmail();
242+
// The default audience posture is invite_only, and the shared server's first
243+
// sign-up is its only uninvited one: every user enters through a pending
244+
// invitation, the lane `@objectstack/verify`'s signUp takes.
245+
await engine.insert(
246+
'sys_invitation',
247+
{
248+
id: `inv_${emailSeq}`,
249+
email,
250+
status: 'pending',
251+
organization_id: 'org_rotation_audience_gate',
252+
role: 'member',
253+
inviter_id: 'usr_rotation_audience_gate',
254+
expires_at: new Date(Date.now() + 3_600_000),
255+
},
256+
{ context: { isSystem: true } } as never,
257+
);
223258
await client.auth.register({ email, password: PASSWORD, name: 'Rotating User' });
224259
const token = storedToken(client);
225260
expect(token, 'register stored no bearer token — the premise of this suite is gone').toBeTruthy();
@@ -304,8 +339,9 @@ const signedCredentialFor = async (
304339
return String(signed);
305340
};
306341

307-
/** Destroy every engine a scenario opened: after each case, and after the warm-up. */
342+
/** Destroy the file's server: after the warm-up, and after the last case. */
308343
const closeEngines = async (): Promise<void> => {
344+
server = undefined;
309345
while (engines.length) {
310346
const e = engines.pop();
311347
try {
@@ -326,10 +362,10 @@ beforeEach(() => {
326362
vi.spyOn(console, 'warn').mockImplementation(() => {});
327363
vi.spyOn(console, 'error').mockImplementation(() => {});
328364
});
329-
afterEach(async () => {
365+
afterEach(() => {
330366
vi.restoreAllMocks();
331-
await closeEngines();
332367
});
368+
afterAll(closeEngines);
333369

334370
// ───────────────────────────────────────────────────────────────────────────
335371
describe("#16534 ① the card's own probe, with the manual re-set deleted", () => {

‎packages/runtime/src/stored-metadata-reader-contexts.pin.test.ts‎

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -416,23 +416,90 @@ async function waitFor(predicate: () => Promise<boolean>, ms = 15_000): Promise<
416416
return false;
417417
}
418418

419+
/** The composition, in `bootStack`'s order (header). `hostPlugins` go where `PIN_HANDLER_PLUGIN` goes. */
420+
async function bootKernel(appConfig: any, hostPlugins: Plugin[]): Promise<any> {
421+
const k: any = new ObjectKernel();
422+
await k.use(new ObjectQLPlugin());
423+
await k.use(new DefaultDatasourcePlugin({ driver: 'sqlite-wasm', config: { filename: ':memory:' } }));
424+
await k.use(new HonoServerPlugin({ port: 0 }));
425+
await k.use(new AppPlugin(appConfig));
426+
await k.use(new PlatformObjectsPlugin());
427+
await k.use(new AuthPlugin({ secret: 'reader-seam-21454-secret', autoDefaultOrganization: false }));
428+
for (const plugin of hostPlugins) await k.use(plugin);
429+
await k.use(new SecurityPlugin(appSecurityPluginOptions(appConfig)));
430+
await k.use(new SharingServicePlugin());
431+
await k.use(createRestApiPlugin({}));
432+
await k.use(createDispatcherPlugin({}));
433+
await k.bootstrap();
434+
return k;
435+
}
436+
437+
/**
438+
* [#23181] The process's first loads, paid during collection rather than in
439+
* `beforeAll` and the first cases' clocks (AGENTS.md: clocked windows measure
440+
* behaviour, never loading). Measured isolated on 4 vCPU: the file's first
441+
* boot is about 1.3 s slower than a second one in the same process, because
442+
* the boot `import()`s better-auth and the services' entries lazily and runs
443+
* every path cold; the first sign-in and the first sandboxed body pay their
444+
* own first use the same way.
445+
*
446+
* One throwaway kernel walks those paths once: a boot, the dev administrator's
447+
* sign-in, and one sandboxed action body. It is stopped before the file's
448+
* `beforeAll` runs. It shares nothing a case asserts on: its app, its database,
449+
* its users and its kernel are its own. What outlives it is the process's
450+
* state: the module registry, sql.js's and the sandbox's compiled WASM, the
451+
* JIT, and each module's once-per-process memos, none of which a case reads.
452+
* `NODE_ENV` is put back as found.
453+
*/
454+
async function warmUpFirstLoads(): Promise<void> {
455+
const before = process.env.NODE_ENV;
456+
process.env.NODE_ENV = 'development'; // the dev-admin seed the sign-in reads, as in `beforeAll`
457+
let k: any;
458+
try {
459+
k = await bootKernel(
460+
{
461+
manifest: { id: 'com.pin.reader21454.warmup', name: 'Warm-up', version: '1.0.0' },
462+
objects: [
463+
{
464+
name: 'warm_note',
465+
label: 'Warm note',
466+
fields: { title: { type: 'text', label: 'Title' } },
467+
actions: [{ name: 'warm_body', label: 'Warm body', type: 'script', body: actionBody('return { ok: true };') }],
468+
},
469+
],
470+
},
471+
[],
472+
);
473+
const raw = (await k.getServiceAsync('http-server')).getRawApp();
474+
const signedIn = await raw.request(`${ORIGIN}${API}/auth/sign-in/email`, {
475+
method: 'POST',
476+
headers: { 'Content-Type': 'application/json' },
477+
body: JSON.stringify(ADMIN),
478+
});
479+
if (!signedIn.ok) throw new Error(`pin warm-up signIn failed: ${signedIn.status} ${await signedIn.text()}`);
480+
const { token } = (await signedIn.json()) as { token: string };
481+
const ran = await raw.request(`${ORIGIN}${API}/actions/warm_note/warm_body`, {
482+
method: 'POST',
483+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
484+
body: JSON.stringify({ params: {} }),
485+
});
486+
if (!ran.ok) throw new Error(`pin warm-up body failed: ${ran.status} ${await ran.text()}`);
487+
} finally {
488+
try { await (await k?.getServiceAsync('http-server'))?.close?.(); } catch { /* best-effort */ }
489+
try { await k?.shutdown?.(); } catch { /* best-effort */ }
490+
if (before === undefined) delete process.env.NODE_ENV;
491+
else process.env.NODE_ENV = before;
492+
}
493+
}
494+
495+
// At module scope, never in a hook: vitest clocks a hook as it clocks a test.
496+
await warmUpFirstLoads();
497+
419498
beforeAll(async () => {
420499
prevNodeEnv = process.env.NODE_ENV;
421500
process.env.NODE_ENV = 'development'; // the dev-admin seed, as `objectstack dev` / bootStack arm it
422501

423-
kernel = new ObjectKernel();
424-
await kernel.use(new ObjectQLPlugin());
425-
await kernel.use(new DefaultDatasourcePlugin({ driver: 'sqlite-wasm', config: { filename: ':memory:' } }));
426-
await kernel.use(new HonoServerPlugin({ port: 0 }));
427-
await kernel.use(new AppPlugin(PIN_APP));
428-
await kernel.use(new PlatformObjectsPlugin());
429-
await kernel.use(new AuthPlugin({ secret: 'reader-seam-21454-secret', autoDefaultOrganization: false }));
430-
await kernel.use(PIN_HANDLER_PLUGIN);
431-
await kernel.use(new SecurityPlugin(appSecurityPluginOptions(PIN_APP)));
432-
await kernel.use(new SharingServicePlugin());
433-
await kernel.use(createRestApiPlugin({}));
434-
await kernel.use(createDispatcherPlugin({}));
435-
await kernel.bootstrap();
502+
kernel = await bootKernel(PIN_APP, [PIN_HANDLER_PLUGIN]);
436503

437504
httpServer = await kernel.getServiceAsync('http-server');
438505
app = httpServer.getRawApp();

0 commit comments

Comments
 (0)