Skip to content

Commit 9d5ebfd

Browse files
test(plugin-audit): one D4 parent-naming case per reader, so no case crosses the 5 s default on a loaded shard (#27634)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_ Part of #27547 Clause-②: no Metadata-protocol: none Skills-docs: none — test-only ## 维护者速读 - 改了什么:plugin-audit 的一个集成测试用例(ADR-0139 D4,原先在一个用例里依次跑三位读者)拆成每位读者一个用例(`it.each`)。断言、夹具、5 秒默认超时都不变。 - 为什么改:4 小时全量跑在 Test Core (3/8) 上 plugin-audit 红了一次,同一提交的 push 跑却是绿的。本地实测:这个用例空闲时 1.3 秒,重放该分片时 4.5 秒,在 3 倍 CPU 超订下两次都 5000ms 超时。拆开后每个用例只做原来三分之一的读。 - 风险与回滚:只改一个测试文件,不发布任何包;回滚就是 revert 这一个提交。 - 先看:`packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts` 第 336 行起的 `it.each` 块,断言行与原来逐字相同。 - 两分钟验证:`pnpm --filter @objectstack/plugin-audit exec vitest run src/parent-record-read-leaf.integration.test.ts`,预期 18 个用例全绿,D4 显示为 MEMBER_A / MEMBER_B / ADMIN 三个用例。 - 需要你做的:无,席位落地。 ## What changes - `[ADR-0139 D4] a read naming its parents is served the rows the same read spelled as an $or is, through one branch per named parent` was one `it` looping over `[MEMBER_A, MEMBER_B, ADMIN]`. It is now an `it.each` over the same three readers, with names ending `— MEMBER_A`, `— MEMBER_B` and `— ADMIN`. The file goes from 16 cases to 18. - Each case runs exactly the loop body the old case ran for its reader. Every `expect`, read and `const` line is identical to the old one apart from indentation (the indent-insensitive diff below prints only the case header and the removed `for` line). The cases share the same fixture (2,300 + 300 documents, 2,100 ledger rows), and the timeout stays at the 5000 ms default. No case is skipped and nothing retries: the #27306 rule. - Why the old case was the exposed one: per reader it did 2 gated finds, a ground-truth pass (5 full-table finds) and a count, over a 2,606-row fixture. That is about 24 full reads in one case, on the 5 s default. ## Tests Before, the old single case, all measured on main ee9a8d6 with 4 CPUs: | Run | D4 case | Verdict | |:--|--:|:--| | idle, whole package | 1259 ms | 1047/1047 green | | replay of the scheduled shard 3: `turbo run test test:repo` over its 11 packages, `--concurrency=4`, `VITEST_MAX_WORKERS=3` | 4466 ms | 1047/1047 green (73/73 tasks, 24m22s) | | 3x CPU oversubscription: `taskset -c 0,1 vitest run --maxWorkers=6`, run a | 6368 ms | red | | 3x CPU oversubscription, run b | 5080 ms | `1 failed / 1046 passed`, `Error: Test timed out in 5000ms.` at `:336` | After, the three new cases: **NOT MEASURED when this PR opened.** The shared verify lock has been held for more than 40 minutes by a sibling's dogfood build and test, and the run that measures this (idle pass, a one-reader ablation per case, and the same 3x probe twice) is queued behind it. Its readings go in the `os-dev-report` on #27547. This PR's own `Test Core` shards run the package on CI meanwhile. <details><summary>Assertion identity: indent-insensitive diff of every expect / read / const line, old case against new</summary> ```text 1c1 < it('[ADR-0139 D4] a read naming its parents is served the rows the same read spelled as an $or is, through one branch per named parent', async () => { --- > ])('[ADR-0139 D4] a read naming its parents is served the rows the same read spelled as an $or is, through one branch per named parent — $label', async ({ reader }) => { 8d7 < for (const reader of [MEMBER_A, MEMBER_B, ADMIN]) { ``` (The windowed comparison also caught one line from past the case's end, `for (const object of [ACTIVITY, LEDGER])`, which is unchanged in the file.) </details> <details><summary>The red this answers: scheduled full run against the same-sha push run</summary> - Scheduled run 40870473883 on d4ff1a0: `Test Core (3/8)` failed only `@objectstack/plugin-audit#test`. `check-test-completeness` reported OK, with 11 of 11 packages and every test accounted for, so vitest finished with a failing case rather than crashing. - Push run 40869443894 on the same sha: all 8 Test Core shards green. The shard 3 drift block reads `@objectstack/plugin-audit: predicted 255.9s, measured 270.6s (1.06x)`. - No file under `packages/plugins/plugin-audit` changed between the last green scheduled run and the red one (`7614f8f0..d4ff1a0`), so no commit introduced it. - The case the red run names could not be read from the dev container: the job log's plugin-audit block sits before the last 5000 lines, and the log blob host is refused by its egress proxy. The case above is the one that times out under load, twice. </details> ## Acceptance notes - `comment-parent-pair.integration.test.ts`, measured only and not changed: 2.8 s idle, 29.5 s on the shard replay's reporter line, 11.2 s as a JSON file window under 3x oversubscription. Its slowest case reaches 1756 ms under 3x, so none of its cases is near the 5 s line, but the file is over the AGENTS.md 10 s file-wall line under load without a declared budget. `parent-record-read-leaf.integration.test.ts` itself is 6.2 s idle and 41.5 s on the replay's reporter line (22.8 s as a JSON window under 3x). Both are noted for the `domain:server` lane, not changed here. - No changeset, `skip-changeset`: plugin-audit ships `files[]` = `dist`, `README.md`, `CHANGELOG.md`. Its built `dist` holds 0 copies of this test's text against 10 of the control symbol `PARENT_GATE_SCAN_LIMIT`, so a `*.test.ts` change publishes nothing. ## Verification - Commit: de73f79 (only commit; base: merge of origin/main at 4a00b2b). - `pnpm exec eslint --no-inline-config packages/plugins/plugin-audit`: exit 0, no output. - No open PR touches the changed file: 43 open PRs scanned, the first 100 files of each; the control grep for a file one of them does touch hits 1. <details><summary>Derived gates: dispatch-gates --commands, then --ran</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 52 commands for the one changed path. Each was run, with its exit code captured before any pipe. 50 exited 0. - `node scripts/check-plugin-teardown-shape.mjs --self-test` first exited 3: its pinned fixture commit 621a487 was not in the shallow clone. After fetching that one commit it exited 0. - `--ran` reconcile: `52 derived, 50 run, 1 NOT-MEASURED, 1 UNRUN`. - NOT-MEASURED: `pnpm check:dual-build-cjs-loads`, exit 3 PREREQUISITE NOT MET. It reads every package's `dist`, and this worktree has no whole-workspace build. - UNRUN: `pnpm check:type-check-debt`. It builds the whole workspace closure under the shared verify lock, and I stopped it rather than queue a full build behind the lock this PR's own proofs are waiting on. - Both run in CI's `Lint & Repo Gates`. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
1 parent bf658d1 commit 9d5ebfd

1 file changed

Lines changed: 23 additions & 17 deletions

File tree

‎packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -333,30 +333,36 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
333333
]));
334334
});
335335

336-
it('[ADR-0139 D4] a read naming its parents is served the rows the same read spelled as an $or is, through one branch per named parent', async () => {
336+
// One case per reader (#27547): the three readers in one case were ~24 full
337+
// reads of the fixture, which crossed the 5 s default on a loaded Test Core
338+
// shard. Each reader is its own case with the same assertions — never a
339+
// raised timeout, a skipped reader or a smaller fixture (#27306).
340+
it.each([
341+
{ label: 'MEMBER_A', reader: MEMBER_A },
342+
{ label: 'MEMBER_B', reader: MEMBER_B },
343+
{ label: 'ADMIN', reader: ADMIN },
344+
])('[ADR-0139 D4] a read naming its parents is served the rows the same read spelled as an $or is, through one branch per named parent — $label', async ({ reader }) => {
337345
const NAMED = [DOC, VAULT, GATED];
338346
const named = { object_name: { $in: NAMED } };
339347
// The same rows, in a spelling the gate does not read as naming its parents: it keeps every branch.
340348
const spelledOut = { $or: NAMED.map((object_name) => ({ object_name })) };
341349
const atRest: Row[] = await engine.find(ACTIVITY, { limit: 100_000, context: SYS });
342350
const parentOf = new Map(atRest.map((r) => [String(r.id), r.object_name]));
343351
const gateConjunct = (where: any) => where.$and[1];
344-
for (const reader of [MEMBER_A, MEMBER_B, ADMIN]) {
345-
seenWhere[ACTIVITY].length = 0;
346-
const rows = await served(ACTIVITY, reader, { where: named });
347-
const narrowed = gateConjunct(seenWhere[ACTIVITY][0]);
348-
expect(rows).toEqual(await served(ACTIVITY, reader, { where: spelledOut }));
349-
const full = gateConjunct(seenWhere[ACTIVITY][1]);
350-
expect(rows).toEqual((await truth(ACTIVITY, reader)).filter((id) => NAMED.includes(parentOf.get(id))));
351-
expect(await engine.count(ACTIVITY, { where: named }, { context: reader })).toBe(rows.length);
352-
// A reader who reads some of the named parents and not others: the vault's rows are its alone.
353-
expect(rows.some((id) => parentOf.get(id) === VAULT)).toBe(reader === ADMIN);
354-
// One branch per named parent — the doc's and the vault's leaves, the gated parent's probe — of the full enumeration's.
355-
expect(narrowed.$or.map((b: any) => b.object_name)).toEqual([DOC, VAULT, GATED]);
356-
expect(narrowed.$or.filter((b: any) => Array.isArray(b.record_id.$in)).map((b: any) => b.object_name)).toEqual([GATED]);
357-
expect(full.$or.length).toBeGreaterThan(NAMED.length);
358-
expect(full.$or.map((b: any) => b.object_name)).toEqual(expect.arrayContaining(NAMED));
359-
}
352+
seenWhere[ACTIVITY].length = 0;
353+
const rows = await served(ACTIVITY, reader, { where: named });
354+
const narrowed = gateConjunct(seenWhere[ACTIVITY][0]);
355+
expect(rows).toEqual(await served(ACTIVITY, reader, { where: spelledOut }));
356+
const full = gateConjunct(seenWhere[ACTIVITY][1]);
357+
expect(rows).toEqual((await truth(ACTIVITY, reader)).filter((id) => NAMED.includes(parentOf.get(id))));
358+
expect(await engine.count(ACTIVITY, { where: named }, { context: reader })).toBe(rows.length);
359+
// A reader who reads some of the named parents and not others: the vault's rows are its alone.
360+
expect(rows.some((id) => parentOf.get(id) === VAULT)).toBe(reader === ADMIN);
361+
// One branch per named parent — the doc's and the vault's leaves, the gated parent's probe — of the full enumeration's.
362+
expect(narrowed.$or.map((b: any) => b.object_name)).toEqual([DOC, VAULT, GATED]);
363+
expect(narrowed.$or.filter((b: any) => Array.isArray(b.record_id.$in)).map((b: any) => b.object_name)).toEqual([GATED]);
364+
expect(full.$or.length).toBeGreaterThan(NAMED.length);
365+
expect(full.$or.map((b: any) => b.object_name)).toEqual(expect.arrayContaining(NAMED));
360366
});
361367

362368
it('D5.1, as amended: a delegated caller reads through the leaf — every row its delegator intersection admits, past the window, a true total', async () => {

0 commit comments

Comments
 (0)