Repository navigation
ci(test-core): publish the slowest test files and packages beside their pinned weights on every run - #17087
Merged
Conversation
Every `Test Core` run now captures per-file vitest durations per shard and the aggregator merges the six into a job-summary table: the slowest 20 test files and the slowest 10 packages, each package's measured seconds beside its pinned weight from scripts/test-shard-timings.json, plus a merged artifact at 14-day retention. Route: parse the streamed vitest module lines. Measured first, because the default reporter appends a duration only when the module has one and this repo sets no slowTestThreshold: on run 34317273493 every file line carried its milliseconds, including files well under the 300ms default. So no second test run and no extra reporter is added to the shard's 30-minute wall. The three-shape task attribution is imported from check-test-completeness.mjs rather than re-derived; a prefix-only parser is blind exactly on the failing task. Report only: every added step carries `if: always()` and `continue-on-error: true`, sits above the shard attestation pair, and the aggregator's steps sit after the verdict they must never delay or redden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
… rule `.gitignore` ignores `test-*.mjs` anywhere in the tree, so the script landed in the previous commit as an untracked file while `git add -A` reported success and every local gate passed against the on-disk copy. In CI the file would simply not exist, and because the step that runs it is deliberately `continue-on-error`, the failure would have surfaced as a permanently empty table rather than as a red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…ummary A job summary is readable in the web UI and by essentially nothing else — there is no REST endpoint that returns one. Without this the only machine-readable copy of the table is the artifact, which needs a download. The log is the copy a seat, a script or `gh run view --log` can read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Found on this feature's own first CI run: all 529 file rows came from a replayed @objectstack/spec log, while the package half had already refused that same package's near-zero execution window. Unlabelled, a duration measured by whichever run filled the cache reads as this run's measurement. A replayed LOG still carries true per-file durations, unlike a replayed task's execution window, which measures nothing — and dropping the rows would empty the table on exactly the runs where the cache is doing its job. So the rows are kept and each carries a `source` cell, with a count beneath the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 9, 2026 08:06
baozhoutao
enabled auto-merge
September 9, 2026 08:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16454
Clause-②: no — the diff adds a
scripts/tool andci.ymlsteps. No schema key, no closed-set member, no registry entry, and no published export: the root manifest isprivate: trueandscripts/is in no package'sfiles[], so the one new export (attributedLines) crosses no published surface.Every
Test Corerun now publishes the slowest 20 test files and slowest 10 packages to the job summary, each package's measured seconds beside its pinned weight fromscripts/test-shard-timings.json, plus a merged artifact at 14-day retention. No test file is changed, added or removed.The measurement that chose the route
The card deliberately did not mandate a capture route, because nobody had checked whether vitest file lines carry a duration at all — this repo sets no
slowTestThresholdanywhere (measured: 0 occurrences outsidenode_modules; control: the same grep hits 3 files inside it).Route (a), parse the stream. The number: 7 of 7. On a real repo suite (
@objectstack/sdui-parser) under a CI-shaped environment, 7 of 7 file lines carried a duration — including a 3 ms one, far under the 300 ms default threshold. The mechanism agrees:BaseReporter.getModuleLogcallsgetDurationPrefix, which returns empty only when the module has noresult.duration;slowTestThresholdpicks the colour, never the presence.Confirmed on real CI rather than only locally — run
34317273493,Test Core (1/6), job102356069873:So no second test run and no extra reporter is added to the shard's 30-minute wall.
Hazards found here that the brief did not name
This is what I swept, not a complete list. I swept the vitest 4.1.11 reporter selection and module-line rendering, the real CI log shapes for two packages, the
.gitignoreinteraction, and the gate familiesscripts/pm/dispatch-gates.mjsderives from this diff. I did not sweep the merge-queue orpush-event shapes of these steps, and I did not re-derive the brief's hazards 3 and 6-9 line by line againstorigin/main.A.
.gitignoreswallowed the new script, and every local gate stayed green..gitignore:91ignorestest-*.mjsanywhere in the tree. The first commit therefore landed without the script whilegit add -Areported success and all 56 derived gates passed against the on-disk copy. In CI the file would not exist — and because the step that runs it is deliberatelycontinue-on-error, that would have surfaced as a permanently empty table, never as a red. Fixed by naming the scriptscripts/report-test-timings.mjs; the file is now tracked.B. Measuring vitest locally inside an AI agent's container measures a reporter CI never uses. vitest 4 picks its default reporter with
resolved.reporters.push([isAgent ? 'agent' : 'default', {}]), andstd-env'sisAgentreadsAI_AGENT,CLAUDECODE,CLAUDE_CODEand friends. This container sets bothCLAUDECODE=1andAI_AGENT=claude-code_..._agent, so a local run silently selects theagentreporter, which prints no passing file lines at all. Measured on one 3-file suite: 1 file line underagent, 3 underdefault— same command, the only difference beingenv -u AI_AGENT -u CLAUDECODE. Anyone answering the card's route question from a local run would have concluded route (a) was impossible. The refusal text names this, because it is also the most likely way this table silently empties later.C. ANSI escapes land between the digits and
ms. CI runs with colour on, and the duration renders asESC[32m 88ESC[2mmsESC[22mESC[39m. A/(\d+)ms/over a raw log matches nothing. Stripping is shared with the sibling guard, not re-spelled.D.
packages/clifile lines carry a vitest project label, and cli is the package the drift column exists for.getEntityPrefixinserts the project name between the state symbol and the path; with colour on it renders as a background-colouredunit— after stripping, a bare word with no delimiter. Real line from the run above:A parser that reads the token after the symbol as the filename loses every cli row — 114 of them in that run. The path is therefore taken as the last token before the
(N tests)group that looks like a test file, never as a fixed position.Both new hazards from the PM's line-number correction are handled
--onlychanges which tasks land in that leg's summary, so per-package seconds are accumulated across all summaries found rather than assuming one shape.test/test:repo), not on the package prefix, so build output cannot be ingested as test results — pinned in the self-test. (That leg is not intest-core.logtoday, which concatenates only the test legs; the property holds anyway.)Report-only, and why every added step is guarded
Test Coreis the required branch-protection context and the card says report only, no red.if: always()+continue-on-error: true. Unguarded, a diagnostics fault would abort the job before the attestation pair ran and evict a green shard from the merge queue — the measured [finding] Merge-queue build for PR #14433 evicted with every test green —actions/upload-artifactFinalizeArtifact returned a non-retryable 403 on two shards at the same second, and the fail-closed attestation gate read them as missing #14469 failure.check:shard-attestationstill reports the pair as the job's last two steps, unguarded.Verify test shard resultswith the same two flags, so they can neither delay, displace nor redden the verdict.needs:—check:aggregator-rosterassertsOS_AGGREGATOR_MEMBERS+OS_AGGREGATOR_NON_MEMBERSequalsneeds:in both directions, and the shard captures are same-run artifacts needing nothing beyond thecontents: readthe job already has.ci.ymlno step this card may add is permitted to fail anything, so the script's--self-testruns in CI (which is whatcheck:self-test-wiredrequires) but its verdict is visible, not enforcing. Giving it teeth means acheck:step inlint.yml. I did not take that widening — it is outside the card's stated scope of "ci.ymlsteps plus one script".Empty capture is NOT MEASURED, never an empty table
An empty table and a run where nothing could be parsed look identical on a summary page, and only the second needs a person. A merge that parsed nothing prints a named refusal and exits
EXIT_PREREQUISITE_NOT_MET, imported fromscripts/import-prerequisite.mjsrather than re-picked as the literal3. Exit 1 stays reserved for findings; this tool never judges, so exit 1 is unreachable and pinned that way.Note: the brief asked for
prerequisiteNotMetText(...). No such export exists onorigin/main—import-prerequisite.mjsexportsreportPrerequisiteNotMet(importerUrl, verdict, measures), which is about a missing npm dependency, not a missing capture. The constant is imported as instructed; the wording is this tool's own.Ablation — the controls fire
The self-test carries a deliberately naive prefix-only reference parser, and three controls assert it gets an answer wrong where the real parser gets it right. Degrading the real attribution to prefix-only, proven on disk (
let pkg = group;occurrences 1 → 0, marker 1; blob15476e45→7ae420db):Restored from
HEAD, blob hash back to15476e45,git diff HEADclean,RESTORED_EXIT=0.Verification
node scripts/report-test-timings.mjs --self-test→ 57 cases across 4 batteries, every battery at or above its pinned floor.node scripts/check-test-completeness.mjs --self-test→self-test OK— its own 79-case battery is what proves the extractedattributedLineswalk is behaviour-identical.turbo run test --summarize --log-order=streamlog: 7 file timings and 1 package window captured, merged and rendered.scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwere run. 52 exit 0; the 4 non-zero are all exit 3 = NOT MEASURED (check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content), each of which readsdist/and says in its own words "Runpnpm buildfirst. ⛔ This is NOT a pass: nothing was measured." This diff touches no package, so those four are unrelated to it.eslint . --no-inline-config --format jsonran over the whole repo: 6409 files, 0 errors, 0 warnings. No narrowing was needed or claimed.Bootstrap window
A
pull_requestrun builds its shards from this ref, so a PR sees a complete table on its first run; there is no back-fill and no window in which a partial merge reads as a whole one. The "parsed X of Y declared" counter under the table states the coverage on every run, including the first.Not done here, deliberately
scripts/test-shard-timings.jsonis read-only in this PR. The large, correct drift it will show for@objectstack/cli(458.15 s pinned against a much larger measurement) is the evidence this table exists to surface, not a defect to fix — refreshing that dataset is #16173/#16220's, explicitly sequenced after this.Generated by Claude Code