Uh oh!
There was an error while loading. Please reload this page.
perf(core): cheapen the authz transport scan so a slow test stops aborting the Test Core shard - #13656
Conversation
… shard
`authz-store-unavailable.test.ts` rebuilt its transport ledger from source
TWICE per run — once in the CONTROL test, once in the SET EQUALITY test — and
each rebuild walked all of `packages/`, `statSync`'d every directory entry, and
read every `.ts` file into a UTF-8 string before discarding 59% of them for
their path. Measured on this tree: 10,152 file opens and 147.5 MB decoded into
transient JS strings per run, against vitest's inherited 5000 ms default. On CI
that timed out, and a timeout ABORTS THE SHARD — so one slow test cost eleven
other packages their entire run, on PRs that never touched authorization.
Four changes, none of which touch what the suite asserts:
- the scaffolding path filter runs BEFORE the read instead of after it. A
path belongs to the result iff it both contains the call and is not
scaffolding, and set intersection does not care which half is tested
first, so the reordering is semantically free: 2,979 of 5,076 files are no
longer read in full only to be thrown away.
- the needle is matched against BYTES. The needle is pure ASCII and an ASCII
byte never occurs inside a multi-byte UTF-8 sequence, so a byte hit and a
decoded-string hit are the same hit — with no 147 MB decode in between.
- `readdirSync(dir, { withFileTypes: true })` answers "is this a directory?"
from the readdir result, replacing 5,926 `statSync` calls. The symlink limb
keeps the old follow-the-link semantics exactly, so a transport behind a
symlinked directory still cannot escape the ledger.
- the enumeration is computed once per PROCESS. It is still rebuilt FROM
SOURCE on every run, which is the guarantee the #13279 ruling requires; it
is simply not rebuilt twice for one answer.
The 5000 ms budget was inherited, never chosen, and was measurably the wrong
budget for a filesystem scan. The two scanning tests now state one explicitly.
It is a budget, not a timing assertion — deliberately not
`expect(elapsed).toBeLessThan(n)`, which on a shared runner is flaky by
construction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
zhuangjianguo
commented
Aug 31, 2026
PM verdict: ACCEPT — releasingFully green at ⭐ The proof this PR exists for
Reviewed independently, not taken on the PR body's word
⭐ The strongest argument, which the PR body undersellsMoving the scaffolding filter ahead of the read doesn't only cut the constant — it changes what the scan grows with. Cost now scales with production file count rather than total file count, and test files are the faster-growing population in this repo (2,979 of 5,076 walked files were scaffolding). That flattens the growth curve, which is the real answer to "is this a postponement rather than a fix?" Zone 2: three falsifications, one of them mineFour of five assumptions returned with the measurement that decided them, and the seat falsified rather than confirmed where the numbers said so:
⭐ Also recorded: concurrent reads at concurrency 48 were measured (285 ms cold vs 255 ms serial) and rejected rather than shipped. Declining complexity on a measurement is the same discipline as adopting it. The 30,000 ms stated budget is ratified per option A, ruled at #13645 comment 5474133014. ReleasingMarked ready and enqueued. ⛔ Not merged by this seat, and no approving review submitted by it — the queue and a human reviewer own that. This unblocks #13630 (dequeued 04:50:50Z on this exact timeout) and #13635 ( Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13645
packages/core/src/security/authz-store-unavailable.test.tsrebuilt its transport ledger from source twice per run, and each rebuild walked all ofpackages/,statSync'd every directory entry, and read every.tsfile into a UTF-8 string — then discarded 59% of them for their path. Under vitest's inherited 5000 ms default that timed out on CI, and a timeout aborts the shard, so one slow test cost eleven other packages their entire run on PRs that never touched authorization.Verified at
9ae177c47c— the commit this PR ships.What actually costs what
The card blamed the full-tree read and offered memoisation. Both readings were measured, and the picture is more specific than either.
walk()—readdirSync+ 5,926statSyncreadFileSync(f, 'utf8')over 5,076 filesThe directory walk was never the problem. Roughly half of the read half was the UTF-8 decode itself — 147.5 MB of file bytes turned into transient JS strings per run, to answer a yes/no question about an ASCII substring.
The repair — four changes, no assertion touched
readFileSync(f)returns bytes with no decode, andBuffer.prototype.includessearches them. The two spellings cannot disagree: the needle is pure ASCII, and an ASCII byte never occurs inside a multi-byte UTF-8 sequence (continuation bytes are all at or above 0x80), so a byte hit and a decoded-string hit are the same hit.readdirSync(dir, { withFileTypes: true })answers "is this a directory?" from the readdir result, replacing 5,926statSynccalls. The symlink limb keeps the old follow-the-link semantics exactly —Dirent.isDirectory()describes the link, not its target, so without it a symlinked directory would stop being descended and a transport behind one would drop out of the ledger's reach.Measured
Wall time of the scan, and of the two tests that use it.
Cold figures drop the page cache before every run, median of 3. Work performed, which is environment-independent and therefore the number that travels:
statSynccalls per walkHeadroom, stated as a multiple
vitest's timeout is per test callback, so the worst single callback is the unit that matters.
That is why the two scanning tests now carry a stated budget of 30,000 ms. The 5000 ms they had was vitest's default for a test that does no I/O; it was inherited, never chosen, and was measurably the wrong budget for a filesystem scan. It is a budget, not a timing assertion — deliberately not
expect(elapsed).toBeLessThan(n), which on a shared runner is flaky by construction and would only re-file this card's successor. The asymmetry justifies generosity: a budget set close to the observed cost buys nothing, while the failure it guards against takes eleven other packages down with it.The guarantee is intact, and that is checked rather than claimed
The ledger is still rebuilt from source on every run — memoisation means compute once per process, never a checked-in list, a snapshot fixture, or a cache keyed on anything that outlives a commit. Demonstrated rather than asserted, by adding a new production file containing
resolveAuthzContext({and re-running:Removing it returns the suite to green (37/37). A transport added later still cannot inherit the old silence.
Unchanged, verbatim:
TRANSPORT_LEDGERand every disposition in it ·toBeGreaterThanOrEqual(8)·toContain('packages/rest/src/rest-server.ts')·toEqual(Object.keys(TRANSPORT_LEDGER).sort())· every per-transport check.git diffshows noexpect(line changed anywhere in the file. Nothing is skipped, disabled, quarantined, allow-listed, deleted or re-baselined. The #13279 semantics are untouched —resolve-authz-context.tsis not in this diff.Verification
Full
packages/coresuite at9ae177c47c:Test Files 46 passed (46)·Tests 1135 passed (1135)— the same totals CI reported when it was1 failed | 45 passed. BothdiscoverTransports()call sites exercised: CONTROL pays the scan (101 ms), SET EQUALITY reads the memoised result (0 ms).Gate families derived from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, all green at9ae177c47c:check:cross-package-test-inputs·check:test-source-alias·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:nul-bytes·check:type-check-coverage·check:doc-authoring·check-comment-mask-adoption·check-ci-filter-parity·check-keyed-text-bounds·check-plugin-teardown-shape·check-undeclared-dep-imports.eslint --no-inline-configon the changed file: 0 errors, 0 warnings.packages/coredeclares notypecheckscript — its type coverage is carried by the ledger, andcheck:type-check-coveragepasses.No changeset —
skip-changesetThe diff is one
.test.tsfile. It publishes nothing from any package, which is the label's own case (AGENTS.md: a changeset is for feature work; pure fixes do not require one). The label is applied on this PR.Generated by Claude Code