Skip to content

test(cli): derive the vitest tier population instead of freezing it in a list - #14717

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-14554-vitest-tiers-partition-queue-eject
Sep 3, 2026
Merged

test(cli): derive the vitest tier population instead of freezing it in a list#14717
os-trump merged 3 commits into
mainfrom
claude/issue-14554-vitest-tiers-partition-queue-eject

Conversation

@os-trump

@os-trump os-trump commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14554

The anchor named a test file. The thing behind the name is a maintenance
duplicate
: INTEGRATION_FILES in packages/cli/vitest.config.ts was a
hand-written copy of a fact already on disk, and the pin held the copy equal to
a predicate re-derived from the tree. That equality is a real invariant — but a
copy of the tree goes stale when someone else lands a qualifying test file,
and the pin that notices runs inside the merge queue, against a main that
is by construction newer than any queued PR's own run. GitHub stacks queue
entries, so one deterministic red ejects everything behind it.

Measured on the anchor: five ejections in a rolling 24 hours, one independent
hit
, four bystanders touching no packages/cli path at all.

The premise, re-verified rather than taken on trust

  • The tiers PR has landed; the pin is on main and both files named in the
    seat's account are in the list today.
  • Re-deriving the predicate over origin/main reproduces that list exactly:
    72 of 230, zero missing, zero stale. So the list is in sync now — which is
    precisely why closing the anchor on that basis would have been true of one
    ejection and false about the shape.

What changed

file role
packages/cli/vitest-tiers.ts new — the predicate, and integrationTestFiles() which derives the population from it
packages/cli/vitest-tiers.fixtures.ts new — sources whose tier is known by construction
packages/cli/vitest.config.ts the literal array becomes integrationTestFiles(__dirname); header rewritten
packages/cli/test/vitest-tiers-partition.test.ts pin re-aimed at what a derivation cannot pin about itself

A qualifying file arriving on main is now classified, not reported.

⛔ Deliberately unchanged: the predicate is SPAWN or KERNEL, character for
character. No tier means anything new, nothing is skipped, quarantined or
softened, and the list is not deleted in favour of one project — the ~24-minute
serialised run that justifies the split (#13504) is untouched.

Population — every unit accounted for

vitest list --filesOnly --project NAME, run on the pre-change base and on this
branch:

tier before after moved
unit 158 158 0
integration 72 72 0
total 230 230 0

The two file lists are byte-identical per tier (diff reports 0 lines).
No test file changes tier in this PR.

Read that as the point, not as a footnote. Nothing is re-partitioned here.
The tiers hold exactly the files they held, in exactly the tiers they held them,
and pnpm test runs exactly what it ran. This is a change to how membership is
MAINTAINED — a robustness change to the instrument — and the identical
populations are the evidence for that claim.

The mechanism, demonstrated in both directions

Both legs use one synthetic stand-in for "a qualifying file that landed on
main" — a test file constructing a kernel query, the same objectQLCtor
signal that produced the real ejection. It is added to the pre-change base tree
and to this branch, byte-for-byte the same file.

Direction 1 — a file merely new on main must no longer redden.

tree pin exit
base (frozen list) 1files that spawn the CLI or boot a kernel/driver but are NOT in INTEGRATION_FILES (add them), naming test/queue-arrival-probe.e2e.test.ts [objectQLCtor]
this branch (derived) 0, and the file is collected into the integration tier (1 hit)

The base failure is the incident reproduced: same assertion, same signal, on a
PR whose own diff is unrelated to the file.

Direction 2 — a file matching NO project must still redden.

The integration project's include was mutated to drop one entry while the unit
project's exclude still named it, so exactly one file falls out of every tier.
Mutation proven on disk by counting the removed and injected text separately
(include: INTEGRATION_FILES, 1 to 0; INTEGRATION_FILES.slice(1) 0 to 1), not
by an editor's exit code. Pin exit 1, on two cases:

  • vitest run collects a different population than the filesystem holds: expected [ …(229) ] to deeply equal [ …(230) ]
  • files the predicate calls integration that vitest did NOT collect into that project

Worth stating precisely: the silent-skip is caught by the disk-versus-collected
case, not by the union case — the root run is the union of the projects, so a
file in no project disappears from both sides of that comparison and only the
filesystem walk still sees it. That is the case doing the load-bearing work, and
it survives this change untouched.

Direction 2 is the one that matters for accepting this. Queue-safety is
trivial to buy by weakening a pin, and that is exactly what must not have
happened here: a file matching NO project still reds, on the case that actually
detects the silent skip. The pin is not skipped, not quarantined, not softened,
and INTEGRATION_FILES still exists and still drives both projects — it is
computed instead of typed.

Restored by blob identity — git hash-object equal to the HEAD blob
(cbadc4ce…), git diff HEAD empty, git status --porcelain empty — under a
trap … EXIT INT TERM on absolute paths. Control re-run on the restored tree:
green, 22 tests.

No rebuild leg is owed, and that is a property of how the pin imports its
subject, not a convenience:
the pin reaches the predicate through a relative
source path that Vite transforms from src text, and vitest list reads
vitest.config.ts as source. Nothing on either path resolves through a
package's exports into dist/, so there is no artifact whose staleness could
green a mutated leg.

What a pin is still for, once the list is derived

Deriving deletes the copy — and with it the accidental second opinion the copy
gave, because config and pin now compute the same answer from the same code and
will always agree. That independence is replaced in kind, not dropped:

  1. Coverage (unchanged): unit and integration partition every test file
    on disk, read from vitest's own resolution. This is the defect the split can
    cause — vitest 4.1.10 silently skips a file matching no project and reports
    the whole-suite run green — and the reason the list can never simply go away.
  2. The derivation reaches vitest (new): the population vitest reports for
    integration versus an independent re-derivation. Entries are handed to
    include/exclude as globs, so a path the walk spells one way and the
    globber reads another lands in the wrong tier while every count still looks
    right. Unlike the old comparison, it cannot fire because a qualifying file
    arrived on main.
  3. The predicate itself (new): 16 fixture sources — one per signal, one per
    false positive the predicate was tuned against (type-only imports, spelling
    lists, comment-only prose, child_process with no entry point) — plus a union
    check that every declared signal has a fixture. Without these, a predicate
    that matched nothing would empty the integration tier, serialise the suite
    back into unit, and leave every population assertion green.
  4. The shape does not regress: a case reds if the config freezes a literal
    list again.

The fixtures live outside test/ on purpose. The predicate reads sources as
text and maskComments leaves string literals intact, so a fixture spelling a
spawn would classify its own host file the moment that host is a
*.test.ts — the pin would move itself into the integration tier and fail its
own last case. The module says so where someone would be tempted to move it.

Cost

The derivation reads and masks all 230 test files once per config load: ~0.42s
cold / ~0.27s warm on this box, of which maskComments is ~0.38s and the
regexes ~0.01s. A raw-text pre-filter would remove most of it and is
deliberately not taken — it would add a second hand-maintained token list
that must track the predicate, which is the exact class of copy this PR deletes.

Verification

Gate union derived on the final tree with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands
(fresh tree, no staleness warning), exit captured before any pipe.

  • 27 of 27 derived gates run. 26 exit 0.
  • node scripts/check-test-completeness.mjsNOT MEASURED, exit 3, in the
    gate's own words: PREREQUISITE NOT MET — this gate grades a saved turbo run test
    log, and no log was named.There is no local log to hand it, so the local reading for this gate is NOT MEASURED. ⛔ It is not a red, and there is nothing here to fix.
  • pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt first
    returned the same prerequisite verdict (this gate reads built output); both
    exit 0 after the build below, so they are measured, not assumed.
  • Build: turbo run build --filter='./packages/*' --filter='./packages/*/*'
    71 successful, 71 total.
  • Unit tier: vitest run --project unit --maxWorkers=2158 test files,
    2092 tests, all passed
    , 288s. Exit captured before any pipe.
  • Integration tier: vitest run --project integration over all 72 files, run
    in three shards under one hold of the shared verify lock (VERDICT command-exit 0, held 29m06s). All three shards green — 21 + 27 + 24 = 72 test files,
    158 + 289 + 120 = 567 tests, all passed
    , zero FAIL lines across the three
    logs. Sharding is not a narrowing: the three shards are a partition of the
    same 72 files the tier collects, and the runner banks each green shard so a
    container cap kill cannot cost completed work.
  • Lint: whole-repo pnpm lint (eslint . --no-inline-config) — exit 0,
    no problems reported. Declared: this one ran OUTSIDE the verify lock, after
    two 9-minute queue timeouts, on the same basis as the 27 gate scripts above —
    the lock's own status text names check:* gate scripts as unlocked sibling
    work it never excludes, and an eslint scan is that class rather than a build
    or a suite.

Every build and test run went through scripts/pm/os-verify-lock.sh, verdict
read from its VERDICT line, never from a bare $?. The lock was heavily
contended (measured holds of 15, 21 and 29 minutes; six queue timeouts on this
card), which is why the integration tier is sharded and resumable.

⚠️ pnpm --filter @objectstack/cli typecheck exits 0 and that verdict is
empty for this diff.
The package's tsconfig.json is include: ["src"], so
tsc --listFiles returns 0 hits for all four files in this PR. It is a true
sentence that says nothing about the change; recorded as NOT MEASURED rather
than claimed as coverage. Pre-existing and filed separately as #14710, not
repaired here.

Why this is worth more than one anchor

While this was in progress the sibling anchor for
test/run-dev-unbuilt-workspace.e2e.test.ts (card #14648, a different file and
not this PR's scope) ejected another of this seat's PRs from the merge queue and
now stands at five independent hits. That is the second merge-queue eviction
this round caused by an instrument rather than by the PR under test — the
class of failure this change closes for the partition pin. The pattern worth
naming: a check that reads the whole tree, runs inside the queue, and compares
it against something frozen at branch-head time will evict bystanders, and the
durable repair is to stop freezing rather than to soften the check.

No changeset

Nothing this PR touches is published: packages/cli ships files: ["dist", …],
and the diff is a test-harness config plus two modules only that config and the
pin import, plus the pin. pnpm check:published-files is green. The PR carries
skip-changeset for that reason.

🤖 Generated with Claude Code

https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

…n a list

`INTEGRATION_FILES` was a hand-maintained copy of a fact already on disk, so
it went stale whenever ANOTHER PR landed a qualifying test file. The pin that
held list == predicate then fired inside the merge queue, against a `main`
newer than any queued PR's own run, ejecting bystanders behind the root entry.

`packages/cli/vitest-tiers.ts` now carries the predicate and derives the
population at config load; the config hands the result to both projects. The
predicate is unchanged (SPAWN or KERNEL) and re-deriving it over this tree
reproduces the last hand-maintained list exactly: 72 of 230 files, none moved.

The pin keeps the coverage cases, gains an end-to-end check that the
derivation reaches vitest, and replaces the independence the frozen list gave
with fixture sources for every signal and every tuned false positive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
The section still described `INTEGRATION_FILES` as an explicit list the pin
re-derives and compares. It now points at `vitest-tiers.ts` as the single
statement of the predicate, records the merge-queue shape the derivation
removes, and reports the measured population identity across the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 35 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via node_modules (literal, a string literal in SKIP_DIRS))
  • content/docs/deployment/cli.mdx (via node_modules (literal, a string literal in SKIP_DIRS))
What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 30 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0a5cd0208ebceadd15f81d2c7e6ff8b806f6a28b — the merge of head d29fbf2a78c6001fb1c864b254e878bf1e79382f into base 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0a5cd0208ebceadd15f81d2c7e6ff8b806f6a28b && git checkout 0a5cd0208ebceadd15f81d2c7e6ff8b806f6a28b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5 d29fbf2a78c6001fb1c864b254e878bf1e79382f && git checkout -B drift-repro 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5 && git merge --no-ff d29fbf2a78c6001fb1c864b254e878bf1e79382f

node scripts/docs-audit/affected-docs.mjs --json 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 75adf11da2a5ca5bae522dfc4209bd3bdd20f4c5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33701101960 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 35 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

This was referenced Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queue-flake anchor: test/vitest-tiers-partition.test.ts

2 participants