Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 40 — spawns the real producer once per single-cause fixture: three in-process kernel boots on a real SQL driver, the first seeding 2,600 comments on 260 docs through the real writers to sit past the 2,000-row window the comment gate replaced; slowest CI reading 28.07 s (run 40204927516, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up; read before this file seeded its comments ten to a doc and paid its first boot at module scope

/**
* [ADR-0139 E7] The comment-thread read gate on the LEAF path, over
Expand Down Expand Up @@ -55,9 +56,19 @@ const DELEGATED_A = { ...MEMBER_A, onBehalfOf: { userId: 'u_delegator' } };
/** The delegator's own leg on `e7_doc` admits every record but these (by name). */
const WITHHELD_FROM_DELEGATOR = ['a0', 'a1', 'a2'];

/** Past the 2,000-row candidate window the leaf replaces. */
const DOCS_OF_A = 2_300;
const DOCS_OF_B = 300;
/**
* Past the 2,000-row candidate window the leaf replaces: 2,300 comments member
* A may read, 300 member B may. The window was over COMMENT rows (the thread
* probe's candidate pre-scan reads `sys_comment`, `READ_SCAN_LIMIT` in
* `comment-access-hooks.ts`), never over parent records, so the comments sit
* ten to a doc. [#23181] One comment per doc wrote 2,600 docs, and each doc
* write pays the CRUD mirror's ledger row and activity row, which nothing in
* this file reads: about 6 s of the file's clock, against well under 1 s for
* the comments themselves.
*/
const COMMENTS_PER_DOC = 10;
const DOCS_OF_A = 230;
const DOCS_OF_B = 30;
const BATCH = 200;

const READ_OPS = new Set(['find', 'findOne', 'count', 'aggregate']);
Expand Down Expand Up @@ -145,6 +156,12 @@ const commentOn = (object: string, id: string, body: string): Row => ({
author_id: 'u_fixture',
});

// [#23181] Pay the first kernel boot at module scope, never in a hook: vitest
// clocks a hook as it clocks a test, and the first boot is the slow one. The
// throwaway kernel registers no fixture object, writes nothing and is shut down
// before any hook runs.
await (await bootKernel(new SqliteWasmDriver({ filename: ':memory:' }))).kernel.shutdown();

describe('[ADR-0139 E7] the comment-thread read gate over the stamped parent pair', () => {
let kernel: ObjectKernel;
let engine: ObjectQL;
Expand Down Expand Up @@ -209,7 +226,8 @@ describe('[ADR-0139 E7] the comment-thread read gate over the stamped parent pai
for (let i = 0; i < DOCS_OF_A; i++) docs.push({ name: `a${i}`, owner: MEMBER_A.userId });
for (let i = 0; i < DOCS_OF_B; i++) docs.push({ name: `b${i}`, owner: MEMBER_B.userId });
const docIds = await insertInBatches(engine, DOC, docs);
await insertInBatches(engine, COMMENT, docIds.map((id, i) => commentOn(DOC, id, `c${i}`)));
await insertInBatches(engine, COMMENT, docIds.flatMap((id, i) =>
Array.from({ length: COMMENTS_PER_DOC }, (_, k) => commentOn(DOC, id, `c${i * COMMENTS_PER_DOC + k}`))));

ids.gone = String((await engine.insert(DOC, { name: 'gone', owner: MEMBER_A.userId }, { context: SYS })).id);
ids.vault = String((await engine.insert(VAULT, { name: 'vault' }, { context: SYS })).id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
// test-budget: 50 — spawns the real producer once per single-cause fixture: two in-process kernel boots on a real SQL driver, the first seeding 2,600 records through the real CRUD mirror (each pays its activity row and its ledger row) to sit past the 2,000-row window the leaf gate replaced; slowest CI reading 35.78 s (run 40204927516, refresh PR #25960's window (hourly runs 40200279306 to 40272142430)) x 1.25, rounded up; read before this file reused its at-rest reads and paid its first boot at module scope

/**
* [ADR-0139 E5] The parent-record read gates on the LEAF path, through a real
Expand Down Expand Up @@ -180,6 +181,12 @@ async function insertInBatches(engine: ObjectQL, object: string, rows: Row[]): P
}
}

// [#23181] Pay the first kernel boot at module scope, never in a hook: vitest
// clocks a hook as it clocks a test, and the first boot is the slow one. The
// throwaway kernel registers no fixture object, writes nothing and is shut down
// before any hook runs.
await (await bootKernel(new SqliteWasmDriver({ filename: ':memory:' }))).kernel.shutdown();

describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
let kernel: ObjectKernel;
let engine: ObjectQL;
Expand All @@ -201,12 +208,30 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
const ledgerOrganizationBound: boolean[] = [];
const ids: Record<string, string> = {};

/**
* [#23181] Every row of an object at rest, as the system reads it — read once
* and reused. No case in this describe writes after `beforeAll`, so each such
* read answered the same rows: the cases made nearly seventy of them, most
* over tables of thousands of rows. Each reuse counts the object first and
* re-reads on a moved count, so a case that adds or removes rows is never
* judged against an older ground truth.
*/
const atRestCache = new Map<string, { count: number; rows: Row[] }>();
const atRestOf = async (object: string): Promise<Row[]> => {
const count = await engine.count(object, {}, { context: SYS });
const hit = atRestCache.get(object);
if (hit && hit.count === count) return hit.rows;
const rows: Row[] = await engine.find(object, { limit: 100_000, context: SYS });
atRestCache.set(object, { count, rows });
return rows;
};

/** Every row at rest, and the ground truth of who may read which, judged by the stand-in's own rules. */
const truth = async (object: string, reader: any): Promise<string[]> => {
const atRest: Row[] = await engine.find(object, { limit: 100_000, context: SYS });
const atRest = await atRestOf(object);
const live = new Map<string, Map<string, Row>>();
for (const parent of [DOC, VAULT, GATED, FAR]) {
const records: Row[] = await engine.find(parent, { limit: 100_000, context: SYS });
const records = await atRestOf(parent);
live.set(parent, new Map(records.map((r) => [String(r.id), r])));
}
const holder = Array.isArray(reader?.systemPermissions) && reader.systemPermissions.includes(LEDGER_AUDIT_CAPABILITY);
Expand Down Expand Up @@ -298,12 +323,12 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
}, 30_000);

it('control: the fixture is past the window, and every row came from its producer', async () => {
const activity: Row[] = await engine.find(ACTIVITY, { limit: 100_000, context: SYS });
const activity = await atRestOf(ACTIVITY);
expect(activity.length).toBeGreaterThan(PARENT_GATE_SCAN_LIMIT);
expect(activity.filter((r) => r.object_name === DOC && r.type === 'created').length).toBe(DOCS_OF_A + DOCS_OF_B + 1);
expect(activity.some((r) => r.object_name === DOC && r.record_id === ids.gone && r.type === 'deleted')).toBe(true);
expect((await truth(ACTIVITY, MEMBER_A)).length).toBeGreaterThan(PARENT_GATE_SCAN_LIMIT);
const ledger: Row[] = await engine.find(LEDGER, { limit: 100_000, context: SYS });
const ledger = await atRestOf(LEDGER);
expect(ledger.filter((r) => r.action === 'config_change').length).toBe(ROWS_ABOUT_NO_RECORD);
});

Expand All @@ -328,7 +353,7 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
});

it('no row is served whose parent the reader may not read, or that no longer exists', async () => {
const all: Row[] = await engine.find(ACTIVITY, { limit: 100_000, context: SYS });
const all = await atRestOf(ACTIVITY);
const byId = new Map(all.map((r) => [String(r.id), r]));
for (const id of await served(ACTIVITY, MEMBER_A)) {
const row = byId.get(id)!;
Expand Down Expand Up @@ -379,7 +404,7 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
const named = { object_name: { $in: NAMED } };
// The same rows, in a spelling the gate does not read as naming its parents: it keeps every branch.
const spelledOut = { $or: NAMED.map((object_name) => ({ object_name })) };
const atRest: Row[] = await engine.find(ACTIVITY, { limit: 100_000, context: SYS });
const atRest = await atRestOf(ACTIVITY);
const parentOf = new Map(atRest.map((r) => [String(r.id), r.object_name]));
const gateConjunct = (where: any) => where.$and[1];
seenWhere[ACTIVITY].length = 0;
Expand Down Expand Up @@ -431,7 +456,7 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
// per parent object through the engine middleware chain, with the
// delegated context. Asked here for every parent pair at rest, so no
// window cuts it; the leaf must serve exactly the rows it admits.
const atRest: Row[] = await engine.find(ACTIVITY, { limit: 100_000, context: SYS });
const atRest = await atRestOf(ACTIVITY);
const pairs = new Map<string, Set<string>>();
for (const row of atRest) {
const parent = parseParentRecord(row, ACTIVITY);
Expand Down Expand Up @@ -488,7 +513,7 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
return Math.round((performance.now() - t0) * 10) / 10;
};
const reading = {
rowsAtRest: (await engine.find(ACTIVITY, { limit: 100_000, context: SYS })).length,
rowsAtRest: (await atRestOf(ACTIVITY)).length,
leafMs: {
memberFind: await time(() => served(ACTIVITY, MEMBER_A)),
memberCount: await time(() => engine.count(ACTIVITY, {}, { context: MEMBER_A })),
Expand Down
Loading