From 60df35e47e3dec5a68efb28f7f708d508e02f93b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 08:25:27 +0000 Subject: [PATCH] test: share repeated boots and declare budgets for 17 server test files Four files booted the same setup in several cases that only read it: the plain bootStack in harness.served-composition (3 to 1), the kernel in sys-user-default-row-scope (10 to 2), and the seeded real stack in meta-history-diff-authoring-door (20 to 1) and meta-draft-read-builder-gate (21 to 1). Each now boots once in a hook; spies and the dataset mock are cleared before each case, as a fresh boot left them. All 17 files take a line-2 test-budget header (20) from their slowest of the five scheduled CI readings x 1.25: what remains is distinct real work. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FEkQ5mAySGuLppSnAtNXaa --- .../scim-deactivation-reconcile-user.test.ts | 1 + .../src/walled-owner-operator-stamp.test.ts | 1 + ...-admin-assignment-boundary.posture.test.ts | 1 + .../src/rls-check-stored-form.test.ts | 1 + ...-stored-list-ordering-fails-closed.test.ts | 1 + .../src/meta-draft-read-builder-gate.test.ts | 41 ++++++++++++------ .../meta-history-diff-authoring-door.test.ts | 43 +++++++++++++------ ...identity-write-refusal.integration.test.ts | 1 + .../src/installed-plugin-loader.test.ts | 1 + ...lugin-driver-external-object.spike.test.ts | 1 + .../src/sandbox/quickjs-runner.test.ts | 1 + ...user-default-row-scope.integration.test.ts | 28 ++++++++---- packages/verify/src/handle.test.ts | 1 + .../src/harness.required-providers.test.ts | 1 + .../src/harness.served-composition.test.ts | 31 +++++++++---- ...d-session-impersonation-target.pin.test.ts | 1 + .../held-session-platform-admin.pin.test.ts | 1 + 17 files changed, 111 insertions(+), 45 deletions(-) diff --git a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts index e57a9298b6d..061266a9bc8 100644 --- a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts +++ b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a pooled real better-sqlite3 engine and a fresh AuthManager per case, each case's SCIM provision, ban, last-administrator and sign-in sequence its subject; 1.8 s in a local run; slowest CI reading 10.3 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up /** * [#14360] SCIM `active: false` disables the account again — end to end, diff --git a/packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts b/packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts index 8c8d64fc622..eaf37cebb3e 100644 --- a/packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts +++ b/packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine (better-sqlite3 :memory:) and AuthManager in each of the 13 real-engine cases, its sign-ups (a production scrypt hash per new password) and the stamp or probe they produce the case's subject, beside 15 pure-function cases; 3.3 s in a local run; slowest CI reading 11.9 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up /** * [#12751] The walled owner-verified stamp — maintainer ruling 2026-08-28 diff --git a/packages/plugins/plugin-security/src/org-admin-assignment-boundary.posture.test.ts b/packages/plugins/plugin-security/src/org-admin-assignment-boundary.posture.test.ts index 5da72f0ccdb..cf34a92d25a 100644 --- a/packages/plugins/plugin-security/src/org-admin-assignment-boundary.posture.test.ts +++ b/packages/plugins/plugin-security/src/org-admin-assignment-boundary.posture.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine and SecurityPlugin per case across three postures, 60 cases that each write and read back the same holder's assignments, so no boot can be shared; 4.3 s in a local run; slowest CI reading 10.9 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up /** * [ADR-0090 D12, amended 2026-10-17] The organization administrator's diff --git a/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts b/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts index 1e19dad4772..0806c23e292 100644 --- a/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts +++ b/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a real ObjectQL engine and SecurityPlugin per (driver, using, check) policy on two SQL driver families (better-sqlite3 and sqlite-wasm, both :memory:), already shared by every cell that names the policy and warmed at module scope, each policy the subject of its cells; 104 cases, 1.6 s in a local run; slowest CI reading 12.8 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up /** * [#21109, ruling A] The row-level write `check` judges a `date`, `datetime` or diff --git a/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts index 6a461a6522b..693f754a968 100644 --- a/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts +++ b/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine and SecurityPlugin per (driver, clause, predicate) on two SQL driver families, 120 boots for 94 cases, each case asserting on the whole stored table of its own boot (nothing stored, the row unchanged), so no boot can be shared; 4.4 s in a local run; slowest CI reading 11.9 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up /** * [#19886 stage 2e] A row-level policy that ORDERS a field against a bound fails diff --git a/packages/rest/src/meta-draft-read-builder-gate.test.ts b/packages/rest/src/meta-draft-read-builder-gate.test.ts index c843db7d23b..e8620352e3d 100644 --- a/packages/rest/src/meta-draft-read-builder-gate.test.ts +++ b/packages/rest/src/meta-draft-read-builder-gate.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one real-stack boot (better-sqlite3 :memory:, the real sys_metadata objects, protocol and routes) seeded with seven saves and shared by the 21 cases, which only read, each case's door reads its subject; slowest CI reading 10.3 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up; read before the 21 identical boots became one /** * [#20338] A PENDING draft — unpublished authoring work — is read only by a @@ -47,7 +48,7 @@ * arrive ungated, is `meta-draft-read-door-census.test.ts`. */ -import { describe, it, expect, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterAll, beforeAll, beforeEach, vi } from 'vitest'; import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; @@ -115,7 +116,7 @@ const DATASET_DRAFT_ONLY = { ...DATASET, name: 'pipeline', label: 'Pipeline' }; const DRAFT_ONLY_TEXT = ['Atlas (draft)', 'nav_atlas_launch_plan', 'Beacon', 'nav_beacon_home']; const liveEngines: ObjectQL[] = []; -afterEach(async () => { +afterAll(async () => { while (liveEngines.length) { try { await liveEngines.pop()?.destroy(); } catch { /* noop */ } } @@ -218,6 +219,20 @@ async function boot() { return { item, list, drafts, datasetQuery, queryDataset }; } +/** + * One boot, seeded once, for every case: after the seeding every case only + * reads (`GET` on the doors, and the dataset door, whose analytics service is + * the `queryDataset` spy and stores nothing), so no case writes what another + * reads. The spy is cleared before each case, as a boot of its own left it. + */ +let shared: Awaited>; +beforeAll(async () => { + shared = await boot(); +}); +beforeEach(() => { + shared?.queryDataset.mockClear(); +}); + const navIds = (doc: any): string[] => (doc?.navigation ?? []).map((e: any) => e.id); const envelope = (res: any) => ({ status: res.statusCode, code: res.body?.error?.code ?? res.body?.code }); const text = (res: any): string => JSON.stringify(res.body ?? null); @@ -233,7 +248,7 @@ const ITEM_SWITCHES: Record> = { describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', () => { for (const [label, query] of Object.entries(ITEM_SWITCHES)) { it(`${label}: a member without an authoring capability reads the PUBLISHED app — the plain read's answer, byte for byte`, async () => { - const { item } = await boot(); + const { item } = shared; const plain = await item('member', 'app', 'atlas'); const res = await item('member', 'app', 'atlas', query); @@ -245,7 +260,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', ( }, 60_000); it(`${label}: a draft-only app answers the member the plain read's absence — 404, never the draft`, async () => { - const { item } = await boot(); + const { item } = shared; const plain = await item('member', 'app', 'beacon'); const res = await item('member', 'app', 'beacon', query); @@ -256,7 +271,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', ( }, 60_000); it(`${label}: every builder reads the draft (the control) — Studio, Setup and a metadata author alike`, async () => { - const { item } = await boot(); + const { item } = shared; for (const who of BUILDERS) { const res = await item(who, 'app', 'atlas', query); expect(res.statusCode, who).toBe(200); @@ -271,7 +286,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', ( } it('?state=draft tells a member nothing about whether a draft exists: an app with none answers the same published read, never NO_DRAFT', async () => { - const { item } = await boot(); + const { item } = shared; const plain = await item('member', 'app', 'quiet'); const res = await item('member', 'app', 'quiet', { state: 'draft' }); @@ -285,7 +300,7 @@ describe('[#20338] GET /meta/:type/:name — ?state=draft and ?preview=draft', ( describe('[#20338] GET /meta/:type — ?preview=draft', () => { it('a member reads the published list — the plain list, byte for byte: no draft-only item, no draft label', async () => { - const { list } = await boot(); + const { list } = shared; const plain = await list('member', 'app'); const res = await list('member', 'app', { preview: 'draft' }); @@ -297,7 +312,7 @@ describe('[#20338] GET /meta/:type — ?preview=draft', () => { }, 60_000); it('every builder reads the draft overlay (the control): the pending draft wins, and the draft-only app is listed', async () => { - const { list } = await boot(); + const { list } = shared; for (const who of BUILDERS) { const res = await list(who, 'app', { preview: 'draft' }); expect(res.statusCode, who).toBe(200); @@ -317,7 +332,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => { for (const [label, bodyFlag, query] of SWITCHES) { it(`${label}: a member's query runs over the PUBLISHED dataset and never asks for draft rows`, async () => { - const { datasetQuery, queryDataset } = await boot(); + const { datasetQuery, queryDataset } = shared; const res = await datasetQuery('member', { datasetName: 'revenue', selection, ...bodyFlag }, query); expect(res.statusCode).toBe(200); @@ -328,7 +343,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => { }, 60_000); it(`${label}: a draft-only dataset answers the member the door's own 404, and nothing is queried`, async () => { - const { datasetQuery, queryDataset } = await boot(); + const { datasetQuery, queryDataset } = shared; const res = await datasetQuery('member', { datasetName: 'pipeline', selection, ...bodyFlag }, query); expect(envelope(res)).toEqual({ status: 404, code: 'NOT_FOUND' }); @@ -336,7 +351,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => { }, 60_000); it(`${label}: an INLINE dataset posted by a member runs over live data, never the pending seed draft's rows`, async () => { - const { datasetQuery, queryDataset } = await boot(); + const { datasetQuery, queryDataset } = shared; const res = await datasetQuery('member', { dataset: { ...DATASET }, selection, ...bodyFlag }, query); expect(res.statusCode).toBe(200); @@ -344,7 +359,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => { }, 60_000); it(`${label}: every builder's query runs over the draft dataset, with the draft rows asked for (the control)`, async () => { - const { datasetQuery, queryDataset } = await boot(); + const { datasetQuery, queryDataset } = shared; for (const who of BUILDERS) { queryDataset.mockClear(); const res = await datasetQuery(who, { datasetName: 'revenue', selection, ...bodyFlag }, query); @@ -362,7 +377,7 @@ describe('[#20338] POST /analytics/dataset/query — previewDrafts', () => { describe('[#20338] one predicate: every draft door admits exactly the callers GET /meta/_drafts admits', () => { it('for each caller, /meta/_drafts refuses them 403 exactly when the item, list and dataset doors serve them the published version', async () => { - const { item, list, drafts, datasetQuery, queryDataset } = await boot(); + const { item, list, drafts, datasetQuery, queryDataset } = shared; for (const who of ['member', ...BUILDERS] as const) { const listing = await drafts(who); const admitted = listing.statusCode !== 403; diff --git a/packages/rest/src/meta-history-diff-authoring-door.test.ts b/packages/rest/src/meta-history-diff-authoring-door.test.ts index a1e4a205c9b..2912dec3c09 100644 --- a/packages/rest/src/meta-history-diff-authoring-door.test.ts +++ b/packages/rest/src/meta-history-diff-authoring-door.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one real-stack boot (better-sqlite3 :memory:, the real sys_metadata objects, protocol and routes) seeded with six saves and shared by the 20 cases, which only read, each case's door reads its subject; slowest CI reading 12.2 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up; read before the 20 identical boots became one /** * [#20378] `GET /meta/:type/:name/diff` and `GET /meta/:type/:name/history` are @@ -62,7 +63,7 @@ * booted stack is `packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts`. */ -import { describe, it, expect, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterAll, beforeAll, beforeEach, vi } from 'vitest'; import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; @@ -138,7 +139,7 @@ type SubjectType = keyof typeof SUBJECTS; const DRAFT_ONLY_TEXT = ['Atlas (draft)', 'nav_atlas_launch_plan', 'Beacon', 'nav_beacon_home', 'Pipeline (draft)', 'Forecast']; const liveEngines: ObjectQL[] = []; -afterEach(async () => { +afterAll(async () => { while (liveEngines.length) { try { await liveEngines.pop()?.destroy(); } catch { /* noop */ } } @@ -244,6 +245,20 @@ async function boot() { return { door, drafts, spies, resetSpies, draftVersion }; } +/** + * One boot, seeded once, for every case: after the seeding every case only + * reads (`GET` on the doors, `draftVersion`'s system read), so no case writes + * what another reads. The spies are cleared before each case, as a boot of its + * own left them (armed after the seeding, with no call recorded). + */ +let shared: Awaited>; +beforeAll(async () => { + shared = await boot(); +}); +beforeEach(() => { + shared?.resetSpies(); +}); + const envelope = (res: any) => ({ status: res.statusCode, code: res.body?.error?.code ?? res.body?.code }); const text = (res: any): string => JSON.stringify(res.body ?? null); const navIds = (doc: any): string[] => (doc?.navigation ?? []).map((e: any) => e.id); @@ -260,7 +275,7 @@ describe('[#20378 · #20441] a member without an authoring capability is refused for (const suffix of AUTHORING_DOORS) { for (const type of Object.keys(SUBJECTS) as SubjectType[]) { it(`${suffix} ${type}: 403 FORBIDDEN in the /meta/_drafts envelope — one answer for a published item, a draft-only one and a missing name, and nothing read`, async () => { - const { door, drafts, spies, resetSpies } = await boot(); + const { door, drafts, spies, resetSpies } = shared; const listing = await drafts('member'); expect(envelope(listing)).toEqual({ status: 403, code: 'FORBIDDEN' }); @@ -298,7 +313,7 @@ describe('[#20378 · #20441] a member without an authoring capability is refused } it('/diff: a range naming the draft save, the default range and an unparseable bound all answer the member the same refusal', async () => { - const { door, draftVersion } = await boot(); + const { door, draftVersion } = shared; const v = await draftVersion('app', 'atlas'); const plain = await door('member', '/diff', 'app', 'atlas'); const ranges: Record[] = [{ from: '0', to: String(v) }, { from: String(v - 1), to: String(v) }, { from: 'abc' }]; @@ -315,7 +330,7 @@ describe('[#20378 · #20441] a member without an authoring capability is refused for (const suffix of ['/history', '/audit'] as const) { it(`${suffix}: an unparseable \`limit\` answers the member the same refusal — decided before the query parse`, async () => { - const { door } = await boot(); + const { door } = shared; const plain = await door('member', suffix, 'app', 'atlas'); const res = await door('member', suffix, 'app', 'atlas', { limit: 'abc' }); expect(envelope(res)).toEqual({ status: 403, code: 'FORBIDDEN' }); @@ -328,7 +343,7 @@ describe('[#20378 · #20441] a member without an authoring capability is refused describe('[#20378 · #20441] builders read /diff, /history and /audit as before — the control', () => { it('/diff app: every builder reads the draft version; the author reads it whole, every other builder pruned as the plain read prunes', async () => { - const { door, draftVersion } = await boot(); + const { door, draftVersion } = shared; const v = await draftVersion('app', 'atlas'); for (const who of BUILDERS) { // [#26462] From the published version, not `from=0`: a version @@ -348,7 +363,7 @@ describe('[#20378 · #20441] builders read /diff, /history and /audit as before }, 60_000); it('/diff view: every builder reads the draft version of a type no per-caller gate judges, and a draft-only view', async () => { - const { door, draftVersion } = await boot(); + const { door, draftVersion } = shared; const v = await draftVersion('view', 'opportunity.pipeline'); for (const who of BUILDERS) { const res = await door(who, '/diff', 'view', 'opportunity.pipeline', { from: String(v - 1), to: String(v) }); @@ -363,7 +378,7 @@ describe('[#20378 · #20441] builders read /diff, /history and /audit as before }, 60_000); it('/history: every builder reads the change log of an app and a view, draft saves included', async () => { - const { door } = await boot(); + const { door } = shared; for (const who of BUILDERS) { for (const [type, name] of [['app', 'atlas'], ['view', 'opportunity.pipeline']] as const) { const res = await door(who, '/history', type, name); @@ -375,7 +390,7 @@ describe('[#20378 · #20441] builders read /diff, /history and /audit as before }, 60_000); it('/audit: every builder reads the audit trail of an app and a view, the author\'s draft save included, and of a draft-only item', async () => { - const { door } = await boot(); + const { door } = shared; for (const who of BUILDERS) { for (const [type, name] of [['app', 'atlas'], ['view', 'opportunity.pipeline']] as const) { const res = await door(who, '/audit', type, name); @@ -397,7 +412,7 @@ describe('[#20378 · #20441] builders read /diff, /history and /audit as before describe('[#20378] /layers and ?layers=true are unchanged for the member — the lit control', () => { it('the member reads both layered doors of a published app: 200, the active row pruned as the plain read prunes it, no draft', async () => { - const { door } = await boot(); + const { door } = shared; const plain = await door('member', '', 'app', 'atlas'); expect(plain.statusCode).toBe(200); const expected = navIds(plain.body?.item); @@ -412,7 +427,7 @@ describe('[#20378] /layers and ?layers=true are unchanged for the member — the }, 60_000); it('the member reads both layered doors of a published view: 200, the active row', async () => { - const { door } = await boot(); + const { door } = shared; for (const [label, suffix, query] of [['/layers', '/layers', {}], ['?layers=true', '', { layers: 'true' }]] as const) { const res = await door('member', suffix, 'view', 'opportunity.pipeline', query); expect(res.statusCode, label).toBe(200); @@ -425,7 +440,7 @@ describe('[#20378] /layers and ?layers=true are unchanged for the member — the describe('[#23546] a setup.access-only caller is refused /diff, /history and /audit, and still lists /meta/_drafts', () => { for (const suffix of AUTHORING_DOORS) { it(`${suffix}: 403 FORBIDDEN in the /meta/_drafts envelope, before any read; /meta/_drafts still answers 200`, async () => { - const { door, drafts, spies, resetSpies } = await boot(); + const { door, drafts, spies, resetSpies } = shared; const listing = await drafts('setupAdmin'); expect(listing.statusCode).toBe(200); const refusal = await drafts('member'); @@ -458,7 +473,7 @@ describe('[#23546] a setup.access-only caller is refused /diff, /history and /au describe('[#23546] the trail admits a strict subset of the callers /meta/_drafts admits', () => { it('every caller the trail admits lists drafts; setup.access alone lists drafts and is refused the trail', async () => { - const { door, drafts } = await boot(); + const { door, drafts } = shared; const admitted: Record = {}; for (const who of Object.keys(CALLERS) as CallerName[]) { const lists = (await drafts(who)).statusCode === 200; @@ -481,7 +496,7 @@ describe('[#23546] the trail admits a strict subset of the callers /meta/_drafts describe('[#26462] /diff refuses a named version with no history row, as /rollback does', () => { it('steps 4–6: both versions missing, one missing, and an item that never existed answer 404 VERSION_NOT_FOUND; the step 3 control answers its change', async () => { - const { door, draftVersion } = await boot(); + const { door, draftVersion } = shared; const v = await draftVersion('view', 'opportunity.pipeline'); const control = await door('studioBuilder', '/diff', 'view', 'opportunity.pipeline', { from: String(v - 1), to: String(v) }); expect(control.statusCode).toBe(200); diff --git a/packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts b/packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts index d5fa6224d61..1ec32e2431e 100644 --- a/packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts +++ b/packages/runtime/src/app-automation-identity-write-refusal.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: a fresh real ObjectQL engine per case, 100 cases that each attempt a governed write and read the store back as unmoved, so no boot can be shared; 3.4 s in a local run; slowest CI reading 10.2 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up /** * [#25653, the #25251 principle for the whole family] App-authored automation diff --git a/packages/runtime/src/installed-plugin-loader.test.ts b/packages/runtime/src/installed-plugin-loader.test.ts index cf419a5d50b..d5cddbea95d 100644 --- a/packages/runtime/src/installed-plugin-loader.test.ts +++ b/packages/runtime/src/installed-plugin-loader.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: two real Node child processes (node --import tsx), each resolving an installed plugin's imports in a process of its own, that being the case's subject (about 1 s each locally), beside 57 in-process cases over real signed tarballs in a temporary OS_HOME; 2.5 s in a local run; slowest CI reading 10.4 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up /** * [#27123 slice 2] The boot-time loader for installed plugins (ADR-0025 §3.5 diff --git a/packages/runtime/src/plugin-driver-external-object.spike.test.ts b/packages/runtime/src/plugin-driver-external-object.spike.test.ts index 7a4b751f023..caa6367cadd 100644 --- a/packages/runtime/src/plugin-driver-external-object.spike.test.ts +++ b/packages/runtime/src/plugin-driver-external-object.spike.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one real-kernel boot in beforeAll (ObjectQL, the sqlite-wasm default datasource, Hono on port 0, auth, datasource admin, security, sharing, REST, the dispatcher and the spike's plugin driver) with real sign-ups, shared by all 12 cases, the boot itself the subject of cases (a) and (b); 2.9 s in a local run, the cases 0.24 s of it; slowest CI reading 12.4 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up /** * Spike: a virtual object on a plugin driver, end to end in a booted kernel diff --git a/packages/runtime/src/sandbox/quickjs-runner.test.ts b/packages/runtime/src/sandbox/quickjs-runner.test.ts index eb1e4bdc970..795e5182375 100644 --- a/packages/runtime/src/sandbox/quickjs-runner.test.ts +++ b/packages/runtime/src/sandbox/quickjs-runner.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: 63 cases on real QuickJS sandboxes, the slowest being real waits that are their subject (600 ms and 500 ms host awaits, 250 ms CPU budgets spent by busy loops, wall-ceiling rollbacks); 4.8 s in a local run; slowest CI reading 10.1 s (scheduled full-battery run 41237094739, Test Core shard log) x 1.25, rounded up import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { QuickJSScriptRunner, SandboxError } from './quickjs-runner.js'; diff --git a/packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts b/packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts index d839953aa83..a2469988569 100644 --- a/packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts +++ b/packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: two real-kernel boots (ObjectKernel with ObjectQL, automation, the identity objects and security on better-sqlite3 :memory:), one per describe, each read by all of its cases through its own producer; slowest CI reading 10.6 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up; read before the ten identical boots became two /** * [#24164 ruling E′; ruling A′ on #24449] `sys_user` declares ONE default row @@ -37,7 +38,7 @@ * `usr_other` belongs to B only; `usr_loner` belongs to none. */ -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; import { ObjectKernel, resolveUserAuthzGrants, type PluginContext } from '@objectstack/core'; import { ObjectQLPlugin, bindHooksToEngine, type ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; @@ -103,7 +104,7 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c /** The context each non-system `sys_user` read carried to the middleware chain. */ let readContexts: Array> = []; - afterEach(async () => { + afterAll(async () => { try { await kernel?.shutdown(); } catch { /* noop */ } }); @@ -179,6 +180,13 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c ], { packageId: 'app:test', logger: silentLogger }); } + // One boot for every case in this block: each case reads through + // its own producer on its own context, and what it asserts on is its own — + // the last `readContexts` entry its read pushed, the hook firing it counted + // from its own `before`. The only rows a case writes are `hook_probe` rows, + // which no case reads; the fixture rows are never written after the boot. + beforeAll(boot); + /** The context the request path hands ObjectQL: `resolveAuthzContext` copies exactly these grants onto it. */ async function requestContext(userId: string, tenantId?: string) { const grants = await resolveUserAuthzGrants(ql, userId, { tenantId, bypassGrantsCache: true }); @@ -224,7 +232,6 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c } it('the premise: the two shipped literals the declaration merges are present and spelled as it joins them', async () => { - await boot(); expect(SHIPPED_LITERALS.map((p) => p.name).sort()).toEqual(['sys_user_org_members', 'sys_user_self']); const declared = (ql.registry.getObject('sys_user') as any).defaultRowScope; const [self, peers] = ['sys_user_self', 'sys_user_org_members'].map((n) => SHIPPED_LITERALS.find((p) => p.name === n)!); @@ -233,7 +240,6 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c for (const [producer, read] of Object.entries(producers)) { it(`${producer}, acting in organisation A: its own row and A's members, nothing beyond, and exactly the literals' union`, async () => { - await boot(); expect(await read(orgA)).toEqual(['usr_me', 'usr_peer']); const context = readContexts[readContexts.length - 1]; expect(Array.isArray(context.org_user_ids), 'the read carries org_user_ids').toBe(true); @@ -241,7 +247,6 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c }); it(`${producer}, acting in no organisation: its own row alone, and exactly the literals' union`, async () => { - await boot(); expect(await read(undefined)).toEqual(['usr_me']); const context = readContexts[readContexts.length - 1]; expect(context.org_user_ids).toEqual(['usr_me']); @@ -250,7 +255,6 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c } it('CONTROL: the rows are the object default\'s — explain names it, and a system read sees all four users', async () => { - await boot(); expect(ids(await ql.find('sys_user', { context: SYS }))).toEqual(['usr_loner', 'usr_me', 'usr_other', 'usr_peer']); type Explained = { layers: Array<{ layer: string; verdict: string; detail: string }> }; const security = kernel.getService<{ explain(input: { object: string; operation: string }, context: object): Promise }>('security'); @@ -272,7 +276,7 @@ describe("sys_user's one `||` default keeps the reader's own row on every user-c */ describe('the registry\'s `extend` rule on a booted composition', () => { let kernel: ObjectKernel; - afterEach(async () => { + afterAll(async () => { try { await kernel?.shutdown(); } catch { /* noop */ } }); @@ -295,8 +299,15 @@ describe('the registry\'s `extend` rule on a booted composition', () => { return kernel.getService('objectql'); } + // One boot for both cases: the first only reads the registry, and + // the second's contribution is refused, leaving the registry as it was (the + // case pins that itself). + let ql: ObjectQL; + beforeAll(async () => { + ql = await bootRegistry(); + }); + it('every `extend` contribution the composition registers is plugin-auth\'s on sys_sso_provider, and it folded', async () => { - const ql = await bootRegistry(); const extensions = ql.registry.getAllObjects().flatMap((o: any) => ql.registry.getObjectContributors(o.name) .filter((c: any) => c.ownership === 'extend') @@ -307,7 +318,6 @@ describe('the registry\'s `extend` rule on a booted composition', () => { }); it('a contribution that sets `defaultRowScope` on sys_user is refused by name; the declared default stands', async () => { - const ql = await bootRegistry(); const declared = (ql.registry.getObject('sys_user') as any).defaultRowScope; expect(declared?.name).toBe('sys_user_self_or_org_members'); const err = (() => { diff --git a/packages/verify/src/handle.test.ts b/packages/verify/src/handle.test.ts index 1d7348b11b3..071a772b2b6 100644 --- a/packages/verify/src/handle.test.ts +++ b/packages/verify/src/handle.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one shared in-process bootStack boot for 22 cases plus two boots that are their cases' subjects (a posture-only walled boot, an unshared boot with automation); 3.7 s in a local run; slowest CI reading 10.0 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up // // hotcrm#1579 step 5a (#15951) — the in-process handle drives the REAL engine. // diff --git a/packages/verify/src/harness.required-providers.test.ts b/packages/verify/src/harness.required-providers.test.ts index 8d6b9326ed0..2869ce0dca9 100644 --- a/packages/verify/src/harness.required-providers.test.ts +++ b/packages/verify/src/harness.required-providers.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: six in-process bootStack boots, no two the same configuration (a top-level requires, a package body's, the top level over the bodies, the control, an explicit instance, triggers with its hard dependencies), each its case's subject; 5.9 s in a local run; slowest CI reading 11.4 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up /** * #22301 item 1 — `bootStack` mounts the providers an app's `requires` names, diff --git a/packages/verify/src/harness.served-composition.test.ts b/packages/verify/src/harness.served-composition.test.ts index 4366c91a03e..2cf3e0075a9 100644 --- a/packages/verify/src/harness.served-composition.test.ts +++ b/packages/verify/src/harness.served-composition.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: five in-process bootStack boots, no two the same configuration (the plain package, read by the slate, cube-control and mail-control cases; a declared cube; a package body's cube; email.persist false; the smtp boot that is refused), the first paying the file's first-boot cost; slowest CI reading 10.9 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up; read before the three plain boots became one /** * #22301 item 1, the composition gap the stage-2 contract review recorded: @@ -23,7 +24,7 @@ * declare is the stack schema's question, not this file's. */ -import { afterEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { CAPABILITY_PROVIDERS } from '@objectstack/core'; import { composeStacks, defineStack } from '@objectstack/spec'; import { PLATFORM_ALWAYS_ON_CAPABILITIES } from '@objectstack/spec/kernel'; @@ -76,6 +77,20 @@ afterEach(async () => { stack = undefined; }); +/** + * The plain one-package boot, booted once for the three cases that boot it: the + * slate case and the cube control only read the kernel, and the mail control's + * send leaves a `sys_email` row that only it counts. + */ +let plain: VerifyStack | undefined; +beforeAll(async () => { + plain = await bootStack(onePackage()); +}, BOOT_TIMEOUT); +afterAll(async () => { + await plain?.stop(); + plain = undefined; +}); + /** The registered plugin name each slate token's provider registers under (the table's first identity). */ const SLATE_PROVIDERS = PLATFORM_ALWAYS_ON_CAPABILITIES.flatMap((token) => { const spec = CAPABILITY_PROVIDERS[token]; @@ -88,13 +103,13 @@ describe('the always-on slate `serve` mounts for every app is mounted', () => { expect(SLATE_PROVIDERS.length).toBeGreaterThanOrEqual(8); const config = onePackage(); expect(config.requires).toBeUndefined(); - stack = await bootStack(config); - const missing = SLATE_PROVIDERS.filter(({ name }) => !stack!.kernel.hasPlugin(name)).map(({ token }) => token); + const booted = plain!; + const missing = SLATE_PROVIDERS.filter(({ name }) => !booted.kernel.hasPlugin(name)).map(({ token }) => token); expect(missing).toEqual([]); // Control: a provider neither declared nor on the slate is not mounted, so // the cases above prove the slate, not the whole table. - expect(stack.kernel.hasPlugin(CAPABILITY_PROVIDERS.approvals!.identities[0]!)).toBe(false); - expect(stack.kernel.hasPlugin(CAPABILITY_PROVIDERS.realtime!.identities[0]!)).toBe(false); + expect(booted.kernel.hasPlugin(CAPABILITY_PROVIDERS.approvals!.identities[0]!)).toBe(false); + expect(booted.kernel.hasPlugin(CAPABILITY_PROVIDERS.realtime!.identities[0]!)).toBe(false); }, BOOT_TIMEOUT); }); @@ -118,8 +133,7 @@ describe('the analytics service is built with the app\'s `analyticsCubes`', () = }, BOOT_TIMEOUT); it('control — an app that declares no cube registers none of this name', async () => { - stack = await bootStack(onePackage()); - expect(await cubeNames(stack)).not.toContain(CUBE); + expect(await cubeNames(plain!)).not.toContain(CUBE); }, BOOT_TIMEOUT); }); @@ -149,8 +163,7 @@ describe('the email service is built from the app\'s mail configuration', () => }, BOOT_TIMEOUT); it('control — with no mail configuration, the same send leaves one row', async () => { - stack = await bootStack(onePackage()); - expect(await probeRowsAfterSend(stack)).toBe(1); + expect(await probeRowsAfterSend(plain!)).toBe(1); }, BOOT_TIMEOUT); it('a mail configuration no transport can deliver through fails the boot, naming the provider and the fix', async () => { diff --git a/packages/verify/src/held-session-impersonation-target.pin.test.ts b/packages/verify/src/held-session-impersonation-target.pin.test.ts index d21845c9826..75142b80a0c 100644 --- a/packages/verify/src/held-session-impersonation-target.pin.test.ts +++ b/packages/verify/src/held-session-impersonation-target.pin.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one in-process bootStack boot in beforeAll shared by all eight cases, plus real better-auth sign-ins, a session per case, since revocation and holds on that session are the subject; 6.2 s in a local run, the boot about 4.3 s of it; slowest CI reading 10.4 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up /** * ADR-0069 on an IMPERSONATION session, on a real boot: which of the TARGET's diff --git a/packages/verify/src/held-session-platform-admin.pin.test.ts b/packages/verify/src/held-session-platform-admin.pin.test.ts index 4905f26db84..ac7b7d768a9 100644 --- a/packages/verify/src/held-session-platform-admin.pin.test.ts +++ b/packages/verify/src/held-session-platform-admin.pin.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 20 — spawns the real producer once per single-cause fixture: one in-process bootStack boot in beforeAll shared by all 13 cases, plus one hold per posture state and real better-auth sign-ins, the held session being the subject; 5.3 s in a local run, the boot most of it; slowest CI reading 10.3 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up /** * ADR-0069 D3 on the platform-admin surface, on a real boot.