Skip to content

Commit b83baed

Browse files
test(ui/scripts): take the vi-mock-inherit test's whole-repo readings once, at collection (#27804)
domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgP6XkfpjCfyzzG2KwQPUZfy)_ Fixes #27449 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`check-vi-mock-inherit.test.ts` 里读真实仓库的三个读数(全量 `scan`、门禁 CLI 一次运行、`@object-ui/i18n` 的 `scan`)每个文件只取一次,在 describe 体(收集阶段)里取,三个慢用例不再在自己的 15 秒预算里重扫整个仓库。 - 为什么改:这三个用例空闲各 5.4–6.0 秒、四核满载下 6.9–7.7 秒,再翻一倍负载就会贴近 15 秒超时,和 #27423 是同一形态。按裁决不调超时、不跳过、不重试、不改断言。 - 风险与回滚:只动一个测试文件,断言逐条不变;读数被冻结,用例之间无法互相污染;读数若抛错,错误被保存并只让读它的那个用例失败,不会在收集阶段弄死整个文件。回滚即 revert 本 PR。 - 先看:`ui/scripts/__tests__/check-vi-mock-inherit.test.ts` 顶部新增的 `realTree` 备忘块(含 `once` / `warm`),以及 `repo state` 和 `the sweep` 两个 describe 体里的取值行。 - 两分钟验证:`pnpm --dir ui exec vitest run scripts/__tests__/check-vi-mock-inherit.test.ts`,预期 83 passed,三个原慢用例各在约 1 毫秒内完成。 - 需要你做的:无,席位落地。 ## What changes `ui/scripts/__tests__/check-vi-mock-inherit.test.ts` only. `scripts/check-vi-mock-inherit.mjs` is untouched: no seam was needed. - A module-level `realTree` memo takes each whole-repository reading once per file: `gate` = `scan(repoRoot)`, `cli` = one run of `node scripts/check-vi-mock-inherit.mjs` over the tree, and `i18n` = `scan(repoRoot, { covered: [I18N], floors: {} })`. Values are frozen (arrays and plain objects, recursively) with the same `frozen()` helper #27437 introduced, so a case that sorted or spliced one in place would throw instead of leaking into the next case. - `repo state — the gate is green on this tree` takes `realTree.gate()` (it already scanned in its body; now through the memo) and warms `realTree.cli` in its body. The case `puts the census in the verdict, so a reader sees the population` reads the held CLI output instead of spawning the CLI itself. - `the sweep — every @object-ui/i18n factory inherits…` warms `realTree.i18n` in its body. `no @object-ui/i18n factory freezes the surface` and `walked a real population…` read the one held scan instead of each running its own. - One addition over #27437's shape: `once()` also holds a reading that THREW and rethrows it to each case that reads it, and `warm()` takes the reading at collection without letting the throw escape. Without it, a red CLI (non-zero exit) would throw during collection and fail the whole file with 0 cases run; before this PR it failed only the census case. Measured below: the failure stays in that one case. ## Why The ruling on the card: take each reading once per file, at collection, through a frozen memo, so no case walks the repo; never raise a timeout, skip, retry or change an assertion. Every `expect` line is byte-identical; only where the value comes from moved. Collection is the right home. `ui/AGENTS.md` (测试纪律) puts warm-up cost where no test or hook timeout bounds it and rules out a `beforeAll`, whose 10 s `hookTimeout` is narrower than the 15 s `testTimeout`. ## Tests Measured on this container (4 cores, a shared box). "Load" means four `node -e 'for(;;){}'` CPU burners beside the run, the method #27437 used. Every run went through `scripts/pm/os-verify-lock.sh`. Per-case durations come from vitest's JSON reporter; the file figure is its `Duration` line. | leg | `puts the census in the verdict…` | `no @object-ui/i18n factory freezes…` | `walked a real population…` | file `Duration` (import / tests) | result | |---|---|---|---|---|---| | before, idle | 5950 ms | 5627 ms | 5415 ms | 25.67 s (6.17 / 19.07) | 83/83 | | before, load | 7748 ms | 6897 ms | 7373 ms | 33.12 s (7.56 / 25.08) | 83/83 | | after, idle | 0.8 ms | 0.2 ms | 0.2 ms | 20.43 s (17.80 / 2.25) | 83/83 | | after, load | 1.0 ms | 0.2 ms | 0.1 ms | 27.61 s (23.82 / 3.27) | 83/83 | After the change the slowest cases in the file are the three `THE DEATH` legs (each spawns a child vitest over a temp root, not the repository): about 0.6 s idle and up to 1.0 s loaded. The collection phase grows by the readings it now takes, about 11.6 s idle and 16.3 s loaded; no timeout bounds that phase. The tests phase drops from 19.07 s to 2.25 s idle and from 25.08 s to 3.27 s loaded, and the file's total wall drops by about 5 s either way. The "after" rows were measured at the final head `2d8468bb` (after the merge of `main`). Failure-locality probe (one run, then restored): the `cli` reading was pointed at a non-existent script, so the CLI exits non-zero. Result: `Tests 1 failed | 82 passed (83)`, the one failure being `puts the census in the verdict…` with `Error: Command failed: node scripts/no-such-gate.mjs`. The file was restored with `git checkout HEAD --` under a trap; proof: `git hash-object` equals `git rev-parse HEAD:<file>`, and `git diff HEAD` is empty. ## Acceptance notes - `ui/` is a private workspace root (`"private": true`) and this is a test file, so nothing publishes: the PR carries `skip-changeset`. - The card's "Done when" readings that need CI runs after merge are the seat's to read. - No assignee and no reviewer were set on this PR: the project's routing rule asks nobody before the seat's ACCEPT and review. ## Verification Gates derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `2d8468bb`, plus `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui run type-check:scripts`. Each command's exit code and the `--ran` reconciliation are in the os-dev report on #27449. <details><summary>Local gate exit codes</summary> ``` node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 pnpm --dir ui run type-check:scripts :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 (the first `vitest run scripts/` run exited 1 before `@objectstack/spec` was built: 16 files failed on `Cannot find package '@objectstack/spec/...'`, none of them this file; after `pnpm --filter @objectstack/spec build` it ran 169 files passed, 2 skipped, 4902 tests passed) dispatch-gates --ran: 18 derived families accounted for, 18 run, 0 NOT-MEASURED ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (CI owns the lint run) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset. This one publishes nothing: it is a test file under the private `ui/` root, so the PR carries `skip-changeset`. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (The 维护者速读 block is the repo's Chinese summary convention.) --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CXUtMecKADTTusrK3z)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 63fc947 commit b83baed

1 file changed

Lines changed: 69 additions & 5 deletions

File tree

‎ui/scripts/__tests__/check-vi-mock-inherit.test.ts‎

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,67 @@ import { maskComments, scanSource } from '../js-comment-mask.mjs';
5353

5454
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
5555

56+
/**
57+
* objectstack#27449 — the whole-repository readings, each taken ONCE per file.
58+
*
59+
* One `scan()` of this repository, or one run of the gate's CLI over it, costs
60+
* seconds, and three cases that each took their own inside the 15 s budget sat
61+
* at 5.4–6 s idle and 6.9–7.7 s loaded. So each `describe` that reads the real
62+
* tree asks for these in its BODY, which runs at collection, and no test or hook
63+
* timeout bounds collection (`ui/AGENTS.md`, 测试纪律: a `beforeAll` would sit
64+
* under the narrower 10 s hook budget instead). The cases only read the values;
65+
* arrays and plain objects are frozen so a case that sorted or spliced one in
66+
* place would throw rather than leak into the next case. Same shape as
67+
* objectstack#27423.
68+
*/
69+
function frozen<T>(value: T): T {
70+
if (Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype) {
71+
for (const member of Object.values(value as object)) {
72+
if (member !== null && typeof member === 'object') frozen(member);
73+
}
74+
Object.freeze(value);
75+
}
76+
return value;
77+
}
78+
79+
/**
80+
* A reading that THREW is held too, and thrown again to each case that reads it:
81+
* a red CLI fails the case that asserts on it, as it did when the case ran it,
82+
* instead of killing the whole file at collection.
83+
*/
84+
function once<T>(read: () => T): () => T {
85+
let held: { value: T } | { error: unknown } | undefined;
86+
return () => {
87+
if (!held) {
88+
try {
89+
held = { value: frozen(read()) };
90+
} catch (error) {
91+
held = { error };
92+
}
93+
}
94+
if ('error' in held) throw held.error;
95+
return held.value;
96+
};
97+
}
98+
99+
/** Take a reading at collection; a failure stays held for the case that reads it. */
100+
function warm(reading: () => unknown) {
101+
try {
102+
reading();
103+
} catch {
104+
// held by `once()`; the case that reads it fails with this error
105+
}
106+
}
107+
108+
const realTree = {
109+
/** The gate's own verdict over this tree, every covered specifier. */
110+
gate: once(() => scan(repoRoot)),
111+
/** The gate's CLI over this tree, as `pnpm check:vi-mock-inherit` runs it. */
112+
cli: once(() => execFileSync('node', ['scripts/check-vi-mock-inherit.mjs'], { cwd: repoRoot, encoding: 'utf8' })),
113+
/** The `@object-ui/i18n` sweep's census (`I18N` is bound by the time its describe collects). */
114+
i18n: once(() => scan(repoRoot, { covered: [I18N], floors: {} })),
115+
};
116+
56117
/** A quote, from its code point — see "Fixture discipline" above. */
57118
const Q = String.fromCharCode(39);
58119

@@ -985,7 +1046,9 @@ describe('non-vacuity — the population refuses to collapse', () => {
9851046
// ---------------------------------------------------------------------------
9861047

9871048
describe('repo state — the gate is green on this tree', () => {
988-
const result = scan(repoRoot);
1049+
// Read at collection, never inside a case (objectstack#27449).
1050+
const result = realTree.gate();
1051+
warm(realTree.cli);
9891052

9901053
it('has no frozen and no unreadable factory on a covered specifier', () => {
9911054
expect(
@@ -1076,7 +1139,7 @@ describe('repo state — the gate is green on this tree', () => {
10761139
// named only the members would claim less than the run actually checked --
10771140
// the same over-claim in the other direction.
10781141
expect(line).toMatch(new RegExp(`\\d+ call site\\(s\\) on ${escapeRe(named)} and their subpaths judged`));
1079-
const out = execFileSync('node', ['scripts/check-vi-mock-inherit.mjs'], { cwd: repoRoot, encoding: 'utf8' });
1142+
const out = realTree.cli();
10801143
expect(out).toMatch(/check-vi-mock-inherit: OK/);
10811144
expect(out).toContain(`${result.census.covered} call site(s) on ${named} and their subpaths judged`);
10821145
});
@@ -1215,10 +1278,11 @@ describe('the sweep — every `@object-ui/i18n` factory inherits, and the specif
12151278
* they are the census the ratchet is computed over.
12161279
*/
12171280

1218-
const swept = () => scan(repoRoot, { covered: [I18N], floors: {} });
1281+
// Read at collection, never inside a case (objectstack#27449).
1282+
warm(realTree.i18n);
12191283

12201284
it('no `@object-ui/i18n` factory freezes the surface', () => {
1221-
const result = swept();
1285+
const result = realTree.i18n();
12221286
expect(
12231287
result.frozen.map((f: { file: string; line: number }) => `${f.file}:${f.line}`),
12241288
'convert these to the obtain-and-spread form — the specifier is covered, so the gate reds too',
@@ -1229,7 +1293,7 @@ describe('the sweep — every `@object-ui/i18n` factory inherits, and the specif
12291293
it('walked a real population — a collapsed scan cannot read as a swept one', () => {
12301294
// Same discipline as `FLOORS`: this describe's green is a claim about 92
12311295
// call sites, so the count is asserted rather than assumed.
1232-
const census = swept().census;
1296+
const census = realTree.i18n().census;
12331297
expect(census.covered).toBeGreaterThan(60);
12341298
expect(census.inherits).toBeGreaterThan(60);
12351299
expect(census.covered - census.inherits - census.automock).toBe(0);

0 commit comments

Comments
 (0)