diff --git a/packages/plugins/plugin-auth/src/cloud-managed.test.ts b/packages/plugins/plugin-auth/src/cloud-managed.test.ts index 20970b32ea8..c7b630562a5 100644 --- a/packages/plugins/plugin-auth/src/cloud-managed.test.ts +++ b/packages/plugins/plugin-auth/src/cloud-managed.test.ts @@ -20,7 +20,7 @@ // SSO-only switched on there, so the narrowing is the managed mode's and not // SSO-only's. -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest'; import { Hono } from 'hono'; import type { PluginContext } from '@objectstack/core'; import { resetCloudManagedForTesting } from '@objectstack/core'; @@ -116,10 +116,11 @@ const makeManager = (engine: ReturnType, extra: Recor } as any); /** - * One deployment, staged UNMANAGED (so its fixture users can sign up through - * the invitation lane), then served through the real mount chain. The tests - * switch the mode on afterwards: every door under test reads the accessor per - * request. + * The staged rows every door case starts from, produced ONCE for the file + * (#23181): staging signs three users up, and each sign-up is a fresh-salt + * production scrypt derivation, the cost that made fourteen identical + * stagings the bulk of this file's wall. Staged UNMANAGED (so the fixture + * users can sign up through the invitation lane), with SSO-only off. * * - `admin`: a platform admin (the legacy `role` scalar, which * `isPlatformAdminUser` accepts) and the owner of the default organization, @@ -128,8 +129,11 @@ const makeManager = (engine: ReturnType, extra: Recor * - `member`: an ordinary `env_native` user holding a local password. * - `idp`: an `idp_provisioned` user with no local credential, the * population the cloud's identity provider owns. + * + * The bearers are the sessions better-auth minted at staging; they live as + * `sys_session` rows, so every copy of the tables carries them. */ -async function stage() { +async function produceStagedRows() { const engine = createMemoryEngine(); engine.tables.set('sys_organization', [{ id: DEFAULT_ORG, name: 'Default', slug: 'default' }]); const manager = makeManager(engine); @@ -187,6 +191,45 @@ async function stage() { users().find((r) => String(r.id) === ids.idp)!.source = 'idp_provisioned'; users().find((r) => String(r.id) === ids.member)!.source = 'env_native'; + return { tables: structuredClone(new Map(engine.tables)), ids, bearers }; +} + +let staged: Awaited>; + +beforeAll(async () => { + const saved = { [MANAGED_ENV]: process.env[MANAGED_ENV], [SSO_ONLY_ENV]: process.env[SSO_ONLY_ENV] }; + setManaged(false); + delete process.env[SSO_ONLY_ENV]; + const quiet = (['info', 'warn', 'error'] as const).map((level) => + vi.spyOn(console, level).mockImplementation(() => {}), + ); + try { + staged = await produceStagedRows(); + } finally { + for (const spy of quiet) spy.mockRestore(); + for (const [key, value] of Object.entries(saved)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + resetCloudManagedForTesting(); + } +}, 120_000); + +/** + * One deployment over its OWN copy of the staged rows: a fresh engine, a fresh + * `AuthManager` and a fresh mount chain per case, exactly as a per-case + * staging gave, so no case sees another's writes. The tests switch the mode + * on afterwards: every door under test reads the accessor per request. + */ +async function stage() { + const engine = createMemoryEngine(); + for (const [name, rows] of structuredClone(staged.tables)) engine.tables.set(name, rows); + const manager = makeManager(engine); + const { ids, bearers } = staged; + + const users = () => (engine.tables.get('sys_user') ?? []) as any[]; + const accounts = () => (engine.tables.get('sys_account') ?? []) as any[]; + const app = new Hono(); const ctx = mockCtx(); const plugin = new AuthPlugin({ secret: SECRET }); diff --git a/packages/plugins/plugin-pinyin-search/src/boot-reconcile.integration.test.ts b/packages/plugins/plugin-pinyin-search/src/boot-reconcile.integration.test.ts index d3a8d49529f..9f6e3a9dbc6 100644 --- a/packages/plugins/plugin-pinyin-search/src/boot-reconcile.integration.test.ts +++ b/packages/plugins/plugin-pinyin-search/src/boot-reconcile.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: real-kernel boots over a SQLite file holding a 2,500-row history, each remaining full walk its case's subject (the shared first walk, a write raced against a held walk, readiness against a held page, a shutdown mid-walk, the ledger-less boots, the forced rebuild); slowest CI reading 15.2 s (scheduled full-battery run 41271604267, Test Core shard log) x 1.25, rounded up; read before the three cases sharing the first walk took a copy of it /** * #25634 — the boot reconcile reads what changed, not the whole table, and @@ -17,8 +18,8 @@ * `find` for the probe object — the cost a boot actually pays. */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { mkdtempSync, rmSync } from 'node:fs'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { copyFileSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ObjectKernel } from '@objectstack/core'; @@ -144,10 +145,12 @@ async function boot(opts: { gate?: Promise; holdFrom?: number; before?: (f: FixturePlugin) => Promise; + /** The database file; the case's own `file` unless named. */ + on?: string; } = {}) { const kernel = new ObjectKernel({ logger: { level: 'silent' } }); kernels.push(kernel); - const fixture = new FixturePlugin(file, opts); + const fixture = new FixturePlugin(opts.on ?? file, opts); const plugin = new PinyinSearchPlugin({ enabled: true }); await kernel.use(new ObjectQLPlugin()); await kernel.use(fixture); @@ -180,28 +183,63 @@ function history(): Array> { } // [#25749] Each case pays for real-kernel boots over a SQLite file (two in -// most) and at least one full 2,500-row reconcile walk; the forced-rebuild -// case walks twice. Idle, the slowest case takes about 1 s. On a 4-core box +// most) and a full 2,500-row walk. Since #23181 the first walk three cases +// share runs once, in the describe's hook; those cases boot over a copy of +// what it left, and the forced-rebuild case still walks every row in its +// explicit rebuild. Idle, the slowest case took about 1 s. On a 4-core box // with 20 busy loops the boots and walks alone took 6.8 s and 10.1 s (seed // excluded), past vitest's 5 s default, so the suite carries its own budget. describe('#25634 the boot reconcile reads what changed since its last completed walk', { timeout: 30_000 }, () => { + // [#23181] The first boot over the 2,500-row history, walked to completion + // with the ledger on, is the same setup in three cases. It runs ONCE, here, + // and its readings are taken while it is up; each of those cases then boots + // over its own copy of the database that walk left (`fromWalked`). + let walkedDir: string; + let walked: string; + let firstReads: Reads; + let firstCjkSearch: unknown; + let firstLatinSearch: unknown; + let firstReceipt: any; + + beforeAll(async () => { + vi.stubEnv('OS_SEARCH_PINYIN_ENABLED', 'true'); + walkedDir = mkdtempSync(join(tmpdir(), 'pinyin-reconcile-walked-')); + walked = join(walkedDir, 'db.sqlite'); + const first = await boot({ on: walked, before: (f) => f.directInsert(history()) }); + try { + await first.plugin.whenReconciled(); + firstReads = { ...first.fixture.reads }; + firstCjkSearch = (await first.fixture.row('p00010'))?.__search; + firstLatinSearch = (await first.fixture.row('p00011'))?.__search ?? null; + firstReceipt = await first.fixture.engine.findOne('sys_migration', { + where: { id: SEARCH_COMPANION_RECONCILE_MIGRATION_ID }, context: SYSTEM, + }); + } finally { + await shutdown(first.kernel); + vi.unstubAllEnvs(); + } + }, 30_000); + + afterAll(() => { + rmSync(walkedDir, { recursive: true, force: true }); + }); + + /** This case's database becomes a copy of the one the first walk left. */ + function fromWalked(): void { + for (const name of readdirSync(walkedDir)) copyFileSync(join(walkedDir, name), join(dir, name)); + } + it('a first boot walks every row; an unchanged second boot reads none — and a receipt sits in sys_migration', async () => { - const first = await boot({ before: (f) => f.directInsert(history()) }); - await first.plugin.whenReconciled(); - const firstReads = { ...first.fixture.reads }; expect(firstReads.rows).toBe(ROWS); expect(firstReads.queries).toBe(Math.ceil(ROWS / PAGE)); // The walk did its job: a CJK history row now carries its companion. - expect((await first.fixture.row('p00010'))?.__search).toBe('zhangwei10 zw10'); - expect((await first.fixture.row('p00011'))?.__search ?? null).toBeNull(); - const receipt: any = await first.fixture.engine.findOne('sys_migration', { - where: { id: SEARCH_COMPANION_RECONCILE_MIGRATION_ID }, context: SYSTEM, - }); - expect(receipt?.blocking).toBe(0); - expect(receipt?.verified_at ?? null).toBeNull(); - expect(JSON.parse(receipt.details).objects[PROBE].sources).toEqual(['title']); - await shutdown(first.kernel); + expect(firstCjkSearch).toBe('zhangwei10 zw10'); + expect(firstLatinSearch).toBeNull(); + expect(firstReceipt?.blocking).toBe(0); + expect(firstReceipt?.verified_at ?? null).toBeNull(); + expect(JSON.parse(firstReceipt.details).objects[PROBE].sources).toEqual(['title']); + fromWalked(); const second = await boot(); await second.plugin.whenReconciled(); // THE PIN: the unchanged table is not read again — one filtered query @@ -212,10 +250,7 @@ describe('#25634 the boot reconcile reads what changed since its last completed }); it('a second boot still fills a row a hook-bypassing writer added since the last walk, reading only that row', async () => { - const first = await boot({ before: (f) => f.directInsert(history()) }); - await first.plugin.whenReconciled(); - await shutdown(first.kernel); - + fromWalked(); const second = await boot({ before: (f) => f.directInsert([{ id: 'p_new', title: '李娜', created_at: new Date().toISOString(), updated_at: new Date().toISOString() }]), }); @@ -236,7 +271,10 @@ describe('#25634 the boot reconcile reads what changed since its last completed }); it('control: the explicit rebuild (force) still walks every row, and fills a row the watermark cannot see', async () => { - const first = await boot({ before: (f) => f.directInsert(history()) }); + // Booted over the walked history: the watermark that walk recorded is the + // one the back-dated write below hides from. + fromWalked(); + const first = await boot(); await first.plugin.whenReconciled(); // A hook- AND stamp-bypassing write that back-dates itself: no watermark // can see it. That is what the explicit rebuild is for. diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts index 7b2d353df36..cb402d98c0e 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.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 on its own better-sqlite3 :memory: database per fixture, most seeded row by row through a natural-order facade (populations of up to 201 users) before the real promotion pass, each population and order its case's subject; slowest CI reading 13.5 s (scheduled full-battery run 41255489006, Test Core shard log) x 1.25, rounded up; read before the card-shape passes were shared by the per-order and same-answer cases /** * Commit 9b9581b11 — WHICH user the `single`-posture bootstrap promotes, and why. @@ -59,7 +60,7 @@ * back to being a function of the driver. */ -import { describe, it, expect, afterEach, beforeEach } from 'vitest'; +import { describe, it, expect, afterEach, beforeAll, beforeEach } from 'vitest'; import { assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { ObjectQL } from '@objectstack/objectql'; import { SqlDriver } from '@objectstack/driver-sql'; @@ -318,33 +319,44 @@ describe('#16682 — the promotion target is chosen, not sampled', () => { // ───────────────────────────────────────────────────────────────────────── describe('the card\'s shape: >50 users, owner last by id and first by created_at', () => { - for (const order of NATURAL_ORDERS) { - it(`promotes the oldest authenticable human under natural order ${order}`, async () => { + // [#23181] One pass per natural order — a fresh engine, the card's + // population seeded through that order's facade, then the promotion — is + // the setup of both the per-order cases and the same-answer case, so it + // runs once per order, here, and the cases read what each pass produced. + const passes = new Map< + NaturalOrder, + { report: Awaited>; info: string[]; grants: string[] } + >(); + + beforeAll(async () => { + delete process.env[OWNER_ENV]; + resetPlatformAdminEmailMemo(); + for (const order of NATURAL_ORDERS) { const engine = await boot(); const ql = withNaturalOrder(engine, order); await seedCardPopulation(ql); const { info, logger } = collectingLogger(); const report = await bootstrapPlatformAdmin(ql, defaultPermissionSets, { logger }); + passes.set(order, { report, info, grants: await adminGrantEmails(engine) }); + } + }, 30_000); + + for (const order of NATURAL_ORDERS) { + it(`promotes the oldest authenticable human under natural order ${order}`, () => { + const { report, info, grants } = passes.get(order)!; expect(report.adminPromoted).toBe(true); expect(report.basis).toBe('oldest-authenticable'); - expect(await adminGrantEmails(engine)).toEqual(['admin@objectos.ai']); + expect(grants).toEqual(['admin@objectos.ai']); expect(info.join('\n')).toContain( 'first user promoted to platform admin: admin@objectos.ai', ); }); } - it('gives the SAME answer under all three orders (the driver may not decide this)', async () => { - const answers: string[] = []; - for (const order of NATURAL_ORDERS) { - const engine = await boot(); - const ql = withNaturalOrder(engine, order); - await seedCardPopulation(ql); - await bootstrapPlatformAdmin(ql, defaultPermissionSets, {}); - answers.push((await adminGrantEmails(engine)).join(',')); - } + it('gives the SAME answer under all three orders (the driver may not decide this)', () => { + const answers = NATURAL_ORDERS.map((order) => passes.get(order)!.grants.join(',')); expect(answers).toEqual(['admin@objectos.ai', 'admin@objectos.ai', 'admin@objectos.ai']); }); diff --git a/packages/rest/src/meta-draft-head-package-inheritance.test.ts b/packages/rest/src/meta-draft-head-package-inheritance.test.ts index 635f093a739..1094cc55151 100644 --- a/packages/rest/src/meta-draft-head-package-inheritance.test.ts +++ b/packages/rest/src/meta-draft-head-package-inheritance.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: 25 cases on 39 real-stack boots (better-sqlite3 :memory:, the real sys_metadata objects, protocol and routes; about 12 ms each, 0.5 s of the file), each case's own save, read, publish and discard sequence on the real /meta routes being its subject, no two the same; slowest CI reading 15.0 s (scheduled full-battery run 41271604267, Test Core shard log) x 1.25, rounded up /** * [#22128] One head row per `/meta` write address: the save's expected-parent diff --git a/packages/verify/src/harness.key-custody.test.ts b/packages/verify/src/harness.key-custody.test.ts index a3f372ffebb..12084b90ab9 100644 --- a/packages/verify/src/harness.key-custody.test.ts +++ b/packages/verify/src/harness.key-custody.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, each the subject of its cases and no two the same setup (an empty key home, a key file already in the home, an OS_SECRET_KEY in the environment, and a restart pair over one database file), every boot in a hook and none warmed up at module scope; slowest CI reading 14.2 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up // // [#21499] `bootStack` never creates key material in the key home, and never // seals under a real key the host already holds. diff --git a/packages/verify/src/harness.posture-only.test.ts b/packages/verify/src/harness.posture-only.test.ts index c8f67c27ffe..90ac53f1495 100644 --- a/packages/verify/src/harness.posture-only.test.ts +++ b/packages/verify/src/harness.posture-only.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: three in-process bootStack boots, no two the same setup (posture-only with no posture in the environment, read by three cases; posture-only under an explicit group posture; the default single boot), the first paying the file's first-boot cost; slowest CI reading 13.8 s (scheduled full-battery run 41199450735, Test Core shard log) x 1.25, rounded up; read before the three identical posture-only boots became one // // [#5261] `bootStack({ multiTenant: 'posture-only' })` — the stand-in that // activates the tenancy POSTURE without the enterprise runtime this package may @@ -22,7 +23,7 @@ // no enterprise package at all, and a mocked one standing by would make that // claim untestable: green whether or not the option actually stands alone. -import { describe, it, expect, afterEach } from 'vitest'; +import { describe, it, expect, afterEach, beforeAll } from 'vitest'; import { bootStack } from './harness.js'; const app = { @@ -51,60 +52,66 @@ afterEach(() => { const BOOT_TIMEOUT = 120_000; describe("bootStack multiTenant: 'posture-only' (#5261)", () => { - it( - 'resolves a real, NON-DEGRADED isolated posture with no enterprise package installed', - async () => { - const stack = await bootStack(app, { multiTenant: 'posture-only' }); - try { - const tenancy = await stack.kernel.getServiceAsync('tenancy'); - expect(tenancy.requestedPosture).toBe('isolated'); - expect(tenancy.posture).toBe('isolated'); - expect(tenancy.isolationActive).toBe(true); - // The load-bearing assertion. `degraded` is exactly "a wall was asked - // for and could not be stood up" — the ADR-0093 D5 state #5261 refuses - // to create organizations in. If the stand-in ever stopped registering - // `org-scoping`, the posture would fall back to `single` + degraded and - // every fixture booting this way would 403 with no explanation. - expect(tenancy.degraded).toBe(false); - } finally { - await stack.stop(); - } - }, - BOOT_TIMEOUT, - ); + // The first three cases read ONE boot (#23181): the same app, the same + // options and no posture in the environment, so three boots were the same + // setup three times. The boot runs in the hook and every reading is taken + // there, while the stack is up (and the posture after `stop()`); a case + // measures behaviour, never loading. + describe('one posture-only boot with no OS_TENANCY_POSTURE set', () => { + let envBeforeBoot: string | undefined; + let envWhileUp: string | undefined; + let envAfterStop: string | undefined; + let tenancy: TenancyShape; + let orgScoping: unknown; - it( - 'registers the `org-scoping` service — the one fact the open core probes', - async () => { + beforeAll(async () => { + envBeforeBoot = process.env.OS_TENANCY_POSTURE; const stack = await bootStack(app, { multiTenant: 'posture-only' }); try { - // Named explicitly because it is a CONTRACT with the enterprise package, - // not an implementation detail: `TenancyService.probeIsolation`, - // SecurityPlugin's RLS-strip decision and `requiresService` nav gating - // all key on this exact service name. A stand-in registering it under - // any other name would satisfy none of them. - const orgScoping = await stack.kernel.getServiceAsync('org-scoping'); - expect(orgScoping).toBeTruthy(); + envWhileUp = process.env.OS_TENANCY_POSTURE; + const live = await stack.kernel.getServiceAsync('tenancy'); + tenancy = { + posture: live.posture, + requestedPosture: live.requestedPosture, + isolationActive: live.isolationActive, + degraded: live.degraded, + }; + orgScoping = await stack.kernel.getServiceAsync('org-scoping'); } finally { await stack.stop(); } - }, - BOOT_TIMEOUT, - ); + envAfterStop = process.env.OS_TENANCY_POSTURE; + }, BOOT_TIMEOUT); - it( - 'restores OS_TENANCY_POSTURE on stop(), like the real multiTenant path', - async () => { - expect(process.env.OS_TENANCY_POSTURE).toBeUndefined(); - const stack = await bootStack(app, { multiTenant: 'posture-only' }); - expect(process.env.OS_TENANCY_POSTURE).toBe('isolated'); - await stack.stop(); + it('resolves a real, NON-DEGRADED isolated posture with no enterprise package installed', () => { + expect(tenancy.requestedPosture).toBe('isolated'); + expect(tenancy.posture).toBe('isolated'); + expect(tenancy.isolationActive).toBe(true); + // The load-bearing assertion. `degraded` is exactly "a wall was asked + // for and could not be stood up" — the ADR-0093 D5 state #5261 refuses + // to create organizations in. If the stand-in ever stopped registering + // `org-scoping`, the posture would fall back to `single` + degraded and + // every fixture booting this way would 403 with no explanation. + expect(tenancy.degraded).toBe(false); + }); + + it('registers the `org-scoping` service — the one fact the open core probes', () => { + // Named explicitly because it is a CONTRACT with the enterprise package, + // not an implementation detail: `TenancyService.probeIsolation`, + // SecurityPlugin's RLS-strip decision and `requiresService` nav gating + // all key on this exact service name. A stand-in registering it under + // any other name would satisfy none of them. + expect(orgScoping).toBeTruthy(); + }); + + it('restores OS_TENANCY_POSTURE on stop(), like the real multiTenant path', () => { + expect(envBeforeBoot).toBeUndefined(); + expect(envWhileUp).toBe('isolated'); // A leaked posture would silently change the NEXT boot in this worker — // the failure mode that makes env-driven fixtures order-dependent. - expect(process.env.OS_TENANCY_POSTURE).toBeUndefined(); - }, - BOOT_TIMEOUT, - ); + expect(envAfterStop).toBeUndefined(); + }); + }); it( 'never overrides an explicit caller-provided OS_TENANCY_POSTURE',