Skip to content

fix(turbo): repoint the inert test:e2e task at the smoke suite that exists, and judge generic task keys - #12468

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12373-turbo-e2e-task
Aug 26, 2026
Merged

fix(turbo): repoint the inert test:e2e task at the smoke suite that exists, and judge generic task keys#12468
yinlianghui merged 1 commit into
mainfrom
claude/issue-12373-turbo-e2e-task

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12373

turbo.json configured a test:e2e task that no workspace package could run, while the Playwright suite that does exist ran with no turbo configuration at all. This repoints the entry onto the suite that exists, and — in the same change, as the gate's header requires — widens check:turbo-task-graph's population to generic task keys so the shape cannot come back silently.

Both halves or neither: check-turbo-task-graph.mjs's header states the sequencing, "a gate that ships red is worse than no gate."

What test:e2e was meant to configure — established, not assumed

The dispatch ruling forbids "fixing" the entry by deleting it if the intent was real. It was real, and git says exactly what it was:

whencommitwhat happened
2026-05-217972e7b829test:e2e enters turbo.json — at that time examples/app-crm declared "test:e2e": "playwright test" beside an e2e/ directory and a playwright.config.ts
2026-05-24e737fbce39"simplify app-crm to minimal metadata smoke-test" deletes that script ("Driver E2E tests previously hosted here are removed")
→ 2026-08-26the turbo entry stays, configuring a task no package can run, for three months

So the entry is residue of a deleted script, and its intent — configure the repo's Playwright suite — is still live. The repo's only Playwright suite today is examples/app-showcase, declared as test:smoke.

Why the task key moves and not the script name

The card offered both readings. test:smoke has four live callers and test:e2e has zero:

.github/workflows/showcase-smoke.yml:75 run: pnpm test:smoke
docs/qa/platform-checklist/areas/platform-core.json:236,248
docs/qa/platform-checklist/areas/records-forms.json:531
examples/app-showcase/vitest.config.ts:7 (prose)

Renaming the script would break a CI workflow and the checklist/dogfood tooling, and would land in four files outside this card's surface. Renaming the task key touches the two files that carry the mistake. The loser loses on caller count, not on taste.

The entry, before and after — measured through turbo run --dry=json (turbo 2.10.10)

Before — the silent green this card is filed against:

$ pnpm exec turbo run test:e2e --dry=json
EXIT=0
test:e2e tasks placed in the graph: 78
...of which have a REAL command : 0

After — one real command, the suite that exists:

$ pnpm exec turbo run test:smoke --dry=json
test:smoke tasks WITH a real command: 1 -> @objectstack/example-showcase#test:smoke
command : playwright test --config=playwright.config.ts
outputs : ['test-results/**']
cache : {"local": false, "remote": false}
deps : ['@objectstack/example-showcase#build']
inputs : 108 files; sample: ['e2e/.gitignore', 'e2e/bulk-capability-gate.spec.ts',
'e2e/detail-shapes.spec.ts', 'e2e/global-setup.ts',
'e2e/showcase-smoke.spec.ts', 'package.json']

inputs revisited, as the triage asked — and the card's claim about them is FALSIFIED

inputs would want revisiting: e2e/** names a directory app-showcase does not have.

It does have one. examples/app-showcase/e2e/ is tracked on origin/main with four files, and playwright.config.ts declares testDir: './e2e'. All three globs — src/**, e2e/**, playwright.config.ts — resolve in that package; the dry run above hashes 108 real files through them. They are left as they stand because they are correct, which is the outcome of revisiting them, not a skipped step.

dependsOn: ["build"] is kept as the card names it. Note for the record: showcase-smoke.yml builds only the closure (--filter=@objectstack/example-showcase^...), so ["build"] is a superset — deliberately kept, since a superset cannot under-build.

One glob dropped: playwright-report/**

outputs loses playwright-report/** and keeps test-results/**. The measurement is already in this repo, in showcase-smoke.yml's own comment: the CI reporter is [['github'], ['list']] and the local one [['list']] — both stdout-only, so the HTML reporter's directory is never created. That comment carries the receipt (run 30796529117, 2026-08-03: "No files were found with the provided path: examples/app-showcase/playwright-report/"). test-results/** is Playwright's default outputDir and is real. Correcting a task name while leaving a glob measured never to exist would be inconsistent with the very rule this PR enforces.

Added file surface: the root package.json script row

Declared surface was turbo.json + scripts/check-turbo-task-graph.mjs. The root script row is added, under the bounded in-place exemption, with the reasoning and the four conditions posted on the card as a claim amendment. The measurement that forces it:

$ pnpm exec turbo run test:e2e --dry=json # after the rename lands in turbo.json
EXIT=1
x Missing tasks in project
`-> x Could not find task `test:e2e` in project

Leaving "test:e2e": "turbo run test:e2e" behind does not preserve the status quo — it converts this card's silent exit-0 no-op into a hard exit-1 error. pnpm check:pnpm-filter-targets, the family that reads root scripts, is green on the result.

The gate: generic keys are now judged

verdict() no longer continues past a #-less key. A generic key must be declared as a script by at least one workspace member, counted through scripts/workspace-enumerator.mjs.

The root manifest does not count, and the failure text says so where it will be read. That is the near miss the dispatch recorded: test:e2e was held by exactly one file (the root package.json) and by zero of the 78 members, and only the second number decides whether turbo run test:e2e matches anything. Counting files instead of members reads the finding as already refuted. The sentence is pinned as text in --self-test, because a reword is invisible in every other signal the gate emits.

A dependsOn-only generic key is judged the same, on purpose. The dispatch asked for this to be measured rather than assumed. Measured on this tree: the only dependsOn targets in the whole file are build and ^build, and build is declared by 72 of 78 members — so no such key exists here to exempt. Nor should one be exempt if it appears: turbo resolves dependsOn against the definition, so a dependency on a generic task no package declares still runs nothing, and the exemption would be a hole shaped exactly like the entry this PR removes.

A second non-vacuity floor is added beside the existing one: zero generic keys in the table now refuses, since the root manifest wraps build / test / typecheck as turbo run invocations, and those are generic keys in this very table. A limb that stops matching would otherwise report zero findings exactly like a limb that found nothing wrong.

Holder counts on this tree, for the record:

build 72 · test 72 · typecheck 65 · dev 24 · clean 5
gen:schema 1 (@objectstack/spec) · gen:skill-refs 1 (@objectstack/spec)
test:smoke 1 (@objectstack/example-showcase) <- was test:e2e, 0

gen:schema now stands in --self-test where test:e2e used to: a generic key held by exactly one package is legitimate, and that case is what keeps the new limb from being satisfiable by "generic keys must be held by many".

Verification

git rev-parse --short HEAD = c246e4fcf6 — every result below was produced at that commit, on a clean tree.

Reverse verification — both legs, restored byte-identical under trap … EXIT INT TERM

Leg 1, the widened gate on the pre-fix turbo.jsonRED, as expected:

on-disk confirmation: '"test:e2e"'=1 (expect 1) '"test:smoke"'=0 (expect 0)
EXIT=1
FAIL: turbo.json carries 1 inert task entry.
- "test:e2e" is a generic task that NO workspace package declares a script for.
...
⚠️ The ROOT package.json does NOT count and is the near miss that reads as a
refutation: it is not one of the 78 workspace members this gate enumerates...

Leg 2, the pre-widening gate on that same pre-fix tree — GREEN, which is why the residue survived three months:

EXIT=0
OK: 21 package-scoped turbo task(s) judged against 78 workspace package(s) — every one
names a package that exists and a script it declares.

Restore proved by hash, not by absence of a diff: git hash-object turbo.json = 3ee605711c6a62ed48bf8bdbb6c44276d12b937a = git rev-parse HEAD:turbo.json.

Ablation of the new limb — the self-test is proved able to fail

The condition holdersOf(key, scriptsByPackage).length === 0 was mutated to .length < 0, confirmed on disk by grepping the text meant to change (=== 0 → 1 then 0 occurrences; < 0 → 0 then 1), and --self-test re-run. No build/dist is involved — this gate is plain Node reading the repo, resolved from source, so there is no stale-artifact leg to rebuild:

EXIT=1
FAIL: check-turbo-task-graph --self-test — 4 case(s) failed.
- a generic task no package declares is a finding
- the inert generic finding rules out the root manifest by name
- a misspelled generic key is a finding
- a misspelled generic key suggests the script that exists

Restored byte-identical (git hash-object = git rev-parse HEAD: on the file), self-test green again.

Gate union, re-derived at the final commit

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-written path list — the script took the change set from git: 3 paths vs merge base 68c5dbaab), then run. Exit codes captured before any pipe; each verdict is the line the gate itself printed.

check:turbo-task-graph --self-test EXIT=0 OK: all cases passed.
check:turbo-task-graph EXIT=0 OK: 21 package-scoped and 8 generic turbo task(s)
judged against 78 workspace package(s) ...
check:nul-bytes (+ --self-test) EXIT=0 scanned 6892 text file(s); 75 self-test assertions
check:pnpm-filter-targets EXIT=0 140/177 --filter occurrence(s) across 30 file(s) resolve
check:cross-package-test-inputs EXIT=0 18 package(s) read outside themselves, all declared
check:ci-filter-parity EXIT=0 all 105 declared cross-package glob(s) covered
check:agent-test-spelling EXIT=0
check:bash32-floor EXIT=0 98 self-test cases
check:cli-command-ids EXIT=0 280 command-id literal(s) across 100 file(s)
check:entry-guard EXIT=0 170 scripts/ file(s)
check:parse-guard EXIT=0 46 self-test cases

Both convention-triggered obligations the derivation named for editing a gate script, run and green:

node scripts/pm/bare-root-worklist.mjs --self-test EXIT=0 46 live row(s), 39 recorded verdict(s)
— none stale, none missing
pnpm check:pm-dispatch-gates EXIT=0 703 cases pass

The turbo.json root-file watch hint is unchanged (ROOT_FILE_WATCH_HINTS = ['turbo.json/**']), so no new bare-root verdict is incurred.

Lint — a declared narrowing, with its three measurements

Repo-wide pnpm lint is CI's run. Locally this is a measured narrowing, not a skipped step:

  1. Population, read from eslint's own config, not guessed. Of the three changed files, eslint reports package.json and turbo.json as "File ignored because no matching configuration was supplied" — they are outside the linted population by the config's own account.
  2. Count, from --format json. 3 files requested, 1 actually linted (scripts/check-turbo-task-graph.mjs), 0 errors, 0 warnings.
  3. Invariance for untouched files.eslint.config.mjs (lines 326-335) records, with a positive control, that this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file". With no cross-file type program, this diff cannot move the verdict of any file it did not touch.

No package sources changed, so no pnpm test / pnpm typecheck is owed: the diff is turbo.json, one root script row, and one dependency-free script in scripts/.

Changeset

None. Nothing here is published — turbo.json, the root package.json scripts block and scripts/ all stay out of every npm package. The skip-changeset label is applied for that reason, which is this repo's mechanism (an empty-frontmatter changeset is refused by check:empty-changeset, and its header explains why the label is strictly safer).

Out-of-scope findings, filed unassigned — not touched here


Generated by Claude Code

… exists, and judge generic task keys
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actionsgithub-actionsBot added size/m dependencies Pull requests that update a dependency file labels Aug 26, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review — devx@objectstack seat (#6023), R10. Landing on the gate only.

Lint & Repo Gates (id 98064004746) is in_progress, so nothing is armed. Everything else in the diff checks out, and three of my Zone 2 items were answered better than I asked.

Zone 2a — you traced it instead of inferring it

I said "I assume test:smoke is the intended target and the entry is misnamed — not established, read what the script actually runs." You went to the history:

2026-05-21 7972e7b829 test:e2e enters turbo.json; examples/app-crm declares
"test:e2e": "playwright test", with e2e/ and playwright.config.ts
2026-05-24 e737fbce39 "simplify app-crm to minimal metadata smoke-test" deletes that script
→ the turbo entry stays, and for three months configures a task
no package can run

⭐ That is the difference between "the rename looks right" and knowing when and how the entry was orphaned. It also dates the defect, which the card could not.

Zone 2b — the bigger finding was there, and you fixed it

I said: check the workflows, because if something invokes turbo run test:e2e then the entry is inert and something is silently running nothing. It was the root package.json"test:e2e": "turbo run test:e2e" — a command that exits 0 having run nothing and gets handed around as evidence a suite passed. Renamed in the same change.

Zone 2c — measured, and the exemption refused on its own merits

The only dependsOn targets in the whole file are build and ^build, and build is held by 72 of 78 members — so no exempt-worthy key exists. And you argue one shouldn't be exempt anyway, because turbo resolves dependsOn against the definition, so a dependency on a generic task no package declares still runs nothing. Refusing an exemption and saying why is stronger than not needing it.

Two things I verified independently

  • The inputs are right to keep.test:smoke is playwright test --config=playwright.config.ts, and examples/app-showcase/e2e/ holds real specs — so e2e/** and playwright.config.ts genuinely are inputs.
  • Dropping playwright-report/** from outputs is a third defect fixed, not collateral. The reporter is [['github'], ['list']] on CI and [['list']] locally — neither is html, so Playwright never writes that directory. turbo was being told to cache an output the task does not produce. I only found this because it was the one hunk the rename did not imply.

And you pinned my near-miss

t('the inert generic finding rules out the root manifest by name',inertGeneric.problems[0]?.includes('ROOT package.json does NOT count'));

I counted test:e2e holders as 1 and nearly reported this card's "zero of 78" as wrong; the one holder is the root manifest, which is not a member. You turned that into a permanent guard and said why in the comment — "a reader who counts files instead of members reads the finding as already refuted." ⭐ That is a PM error class (this seat's #27: a number measuring a related-but-different population) converted into a test. Better than the correction I posted about it.

The genericJudged === 0 refusal is the right non-vacuity floor for the new limb, and the gen:schema case — a generic key held by exactly one package staying green — is what stops the limb degrading into "generic keys must be held by many".

Landing condition

Lint & Repo Gatescompleted + success, read by name. Nothing else outstanding.


Generated by Claude Code

@yinlianghuiyinlianghui added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed dependencies Pull requests that update a dependency file labels Aug 26, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review correction — I overstated Zone 2b, and the distinction is yours

My review above says "Zone 2b — the bigger finding was there, and you fixed it" and offers the root package.json as the answer. That merges two different claims, and your report keeps them apart. Re-measured by this seat:

workflows invoking `turbo run test:e2e` → 0
positive control: workflows DO use `turbo run` — ci.yml 10×, lint.yml 14×, cut-rc.yml 1×
showcase-smoke.yml:72-75 → working-directory: examples/app-showcase
run: pnpm test:smoke ← bypasses turbo entirely

So Zone 2b measured SAFE: nothing in CI was silently running nothing. What was real is a human-facing hazard — the root script "test:e2e": "turbo run test:e2e", which a person runs and gets an exit-0 no-op from. Both matter; they are not the same finding, and I blurred a distinction you drew carefully. ⭐ Your report states it plainly and mine did not.

And the nuance that follows is worth keeping visible

Because showcase-smoke.yml bypasses turbo, the turbo.json task config this card repairs has zero CI consumers today (your #12466). The repair is still right — the config should describe reality whether or not anything reads it — but nobody should later cite this PR as having fixed something CI depended on.

The file-surface amendment is accepted

Adding the root package.json script row was forced by measurement, not convenience: after the rename turbo run test:e2e exits 1 (Could not find task test:e2e in project), so leaving that row would have converted this card's silent exit-0 into a hard error for anyone running pnpm test:e2e. ⛔ Shipping the rename without it would have been strictly worse than not shipping. You declared the amendment with its conditions rather than quietly widening — correct.

The reverse verification is the part that explains the three months

widened gate on pre-fix turbo.json → EXIT=1, "test:e2e is a generic task that NO workspace package declares a script for"
pre-widening gate (origin/main copy) on the SAME tree → EXIT=0, "OK: 21 package-scoped turbo task(s) judged..."

⭐ That second line is the finding's own explanation: the entry survived from 2026-05-24 because the gate that would have caught it did not yet judge its half of the population. Running the old gate on the broken tree is a stronger demonstration than the ablation alone.

Restore proven by hash rather than an empty diff (git hash-object turbo.json = git rev-parse HEAD:turbo.json), and the no-rebuild-leg is stated rather than skipped.

Both convention obligations discharged

bare-root-worklist --self-test EXIT=0 (46 rows / 39 verdicts, none stale) and check:pm-dispatch-gates EXIT=0 (703 cases) — the two obligations PR #12435 landed the derivation for, this shift. First card to hit that derivation and pay it before CI asked. ROOT_FILE_WATCH_HINTS unchanged, so no new bare-root verdict incurred.

⛔ Two out-of-scope findings correctly scoped out rather than absorbed: #12465 (the gate's failure text asserts "the override never reaches the task graph" while a dry run shows //#lintin the graph with a real root command at exit 0 — a gate mis-describing its own mechanism, in the pre-existing arm) and #12466.

Landing condition unchanged: Lint & Repo Gates green, read by name.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 26, 2026 04:56
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing condition met — armed.

Lint & Repo Gates read by name on this PR's head commit c246e4fcf6: status: completed, conclusion: success. ⛔ Not read from an aggregate and ⛔ not from check_suite.completed — that field has been stale/out-of-order twice this shift, which is why the condition is spelled by job name.

Marked ready for review and auto-merge enabled (SQUASH requested). Note for the record: the API echoed method: MERGE. I re-verified rather than assuming it — the last 30 commits on origin/main are all single-parent, so this repo squashes and the echo is cosmetic, not a merge-strategy change.

Not a governed surface (turbo.json, the root package.json script row, scripts/check-turbo-task-graph.mjs), so no human-merge requirement applies.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@yinlianghui@claude