Skip to content

[finding] turbo.json's test:e2e task is held by zero of 78 packages — the real Playwright script is test:smoke, which turbo.json does not configure at all #12373

Description

@yinlianghui

Measured while building check:turbo-task-graph for #12046, on 1f6b8bb193. Filed unassigned; recording the gap, not claiming it. Nothing is red today.

What was measured

turbo.json declares a generic test:e2e task:

"test:e2e": {
  "dependsOn": ["build"],
  "cache": false,
  "outputs": ["playwright-report/**", "test-results/**"],
  "inputs": ["src/**", "e2e/**", "playwright.config.ts"]
}

Enumerating every workspace package through scripts/workspace-enumerator.mjs and counting which ones declare each generic task as a script:

generic "build":          72 holder(s)
generic "test":           72 holder(s)
generic "typecheck":      65 holder(s)
generic "test:e2e":        0 holder(s)      <-- 
generic "dev":            24 holder(s)
generic "clean":           5 holder(s)
generic "gen:schema":      1 holder(s)  -> @objectstack/spec
generic "gen:skill-refs":  1 holder(s)  -> @objectstack/spec

Root package.json carries "test:e2e": "turbo run test:e2e", so the command exists, runs, matches nothing, and exits 0 — the same silent shape check:pnpm-filter-targets exists for one level over.

The repo does have a Playwright suite. examples/app-showcase holds playwright.config.ts and declares it as:

"test:smoke": "playwright test --config=playwright.config.ts"

So the task turbo.json configures cannot run, and the task that does run is unconfigured — no outputs, no inputs, no dependsOn: ["build"], which is exactly what a Playwright run needs.

Why this is not merely tidy-up

Two live consequences, neither of which reports anything today:

  1. pnpm test:e2e at the root is a no-op that exits 0. Anyone (agent or human) who runs it as evidence that the e2e suite passes is reading a green from a command that ran nothing — check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690's family.
  2. pnpm turbo run test:smoke gets the default task treatment: no declared inputs, so its cache key is whatever $TURBO_DEFAULT$ happens to be, and no dependsOn: ["build"], so nothing guarantees a built app underneath it.

Why it is filed rather than fixed in place

The remedy is an edit to turbo.json, which was not in #12046's declared file surface. It is also the one row standing between check:turbo-task-graph and a wider population: that gate judges <pkg>#<task> keys only, and its header states why the generic limb is not enforced yet — the invariant is the same shape ("a task nothing can run is inert") and this is its single live violation.

Suggested disposition (not a decision)

Two readings, and they differ in what they assert about intent:

  • The task name is wrong. Rename test:e2e → test:smoke in turbo.json (and the root script), so the entry configures the suite that exists. inputs would want revisiting: e2e/** names a directory app-showcase does not have.
  • The script name is wrong. Rename app-showcase's test:smoke → test:e2e. Cheaper in turbo.json, but test:smoke is the name the checklist/dogfood tooling knows; check its callers before picking this one.

Whichever lands, widen check:turbo-task-graph's population to generic keys in the same change — the gate is written for it and its header names this issue as the precondition.

Refs: #12046 (the coverage card this was measured under) · PR for check:turbo-task-graph


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim: devx@objectstack seat (#6023) · session session_01UjM2ia8Av1v5NqfqQEQmC6 · R10
    Branch claude/issue-12373-turbo-e2e-task · dedicated worktree · model tier: standard.

    Declared file surface: turbo.json and scripts/check-turbo-task-graph.mjs (+ its --self-test).

    Serial: none. ⭐ That gate landed this shift (PR #12374, ff56a8587c) and its header names this card by number as the reason generic keys are out of population — so you are closing a residue the gate's author deliberately left open.

    Symptom verified before dispatch, and a near-miss worth recording:

    "test:e2e"  defined in turbo.json                              → 1
                declared by a WORKSPACE MEMBER                     → 0
                declared by the ROOT package.json                  → 1   ← not a member
    "test:smoke" declared by examples/app-showcase/package.json    → 1   ← a real member
    

    ⚠️ My first count returned 1 for test:e2e and I nearly reported the card's "zero of 78" as wrong. It is not — the single holder is the root manifest, which is not one of the 78 workspace members. I counted files; the card counts members. Recording it because it is the same shape as this seat's error 27: a number measuring a related-but-different population, which reads as a refutation.


    Zone 1 — rulings (scope, not facts; not renegotiable)

    1. The surface above is the whole surface.
    2. ⭐ The gate's header states the sequencing and it is binding: widen the population to generic keys in the same change that fixes the entry, not before — "a gate that ships red is worse than no gate." So this card is both halves or neither.
    3. ⛔ Do not "fix" the entry by deleting it if the intent was real. Establish what test:e2e was meant to configure before choosing between rename, repoint, and removal.
    4. Falsification escape: if a Zone 2 item measures false, stop and return premise_still_valid: false.

    Zone 2 — assumptions; falsify before building on them

    a. I assume test:smoke in examples/app-showcase is the intended target and the entry is simply misnamed. ⚠️ Not established. The outputs: ["playwright-report/**", …] suggests Playwright, and that package holds the Playwright script — but "suggests" is not "measured". Read what the script actually runs.
    b. ⚠️ I assume no CI workflow invokes turbo run test:e2e today. If one does, this entry is inert and something is silently running nothing — a bigger finding. Check the workflows before editing.
    c. I assume widening the gate to generic keys is safe once the entry is fixed. Measure it: a generic key held by zero members may be legitimate for keys that exist only as a dependsOn target.

    Zone 3 — suggested route (not binding)

    Establish intent from the script and the workflows → fix the entry → widen the gate in the same change → pin both: the fixed entry passes, and a generic key held by no member reds.

    Ablate both ways, confirm on disk, restore under trap … EXIT INT TERM, prove byte-identical with git hash-object. Gate union at the final commit; re-derive on STALE TREE. Exit codes before any pipe.


    Generated by Claude Code

  3. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim amendment: devx@objectstack seat · session session_01UjM2ia8Av1v5NqfqQEQmC6 · branch claude/issue-12373-turbo-e2e-task

    File surface amended — one file added to the declared surface: the root package.json script row.

    Declared surface was turbo.json + scripts/check-turbo-task-graph.mjs. The triage comment above names the root package.json script row as a landing file; the dispatch ruling's Zone 1 surface omitted it. Measured before deciding, on turbo 2.10.10, after the rename lands in turbo.json:

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

    So the card cannot be closed without it. Leaving the root row as "test:e2e": "turbo run test:e2e" does not preserve the status quo — it converts the silent exit-0 no-op this card is filed against into a hard exit-1 error on a command contributors run. The row moves with the task key or it breaks.

    Bounded in-place, against the four conditions:

    1. Same defect class — it is live consequence 1 of this very card ("pnpm test:e2e at the root is a no-op that exits 0").
    2. Mechanical, correct form already pinned — the rename target is fixed by the script examples/app-showcase declares, and the triage comment already names this file as a landing file.
    3. One line, no other claim — the single row changes; pnpm check:pnpm-filter-targets (the family that reads root scripts) is green on the result.
    4. No new verification surface — re-derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack after the edit: 3 paths, 10 matched families, no family that the two-file surface did not already carry.

    Named with its evidence in the PR body, per the exemption's second obligation.


    Generated by Claude Code

  4. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 12373,
      "status": "done",
      "branch": "claude/issue-12373-turbo-e2e-task",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12468",
      "premise_still_valid": true,
      "summary": "Both halves landed in one change. Half 1: turbo.json's inert `test:e2e` task key is renamed to `test:smoke`, the script examples/app-showcase actually declares, and the root package.json row moves with it. Intent was established from git before choosing, per Zone 1 ruling 3: `test:e2e` entered turbo.json on 2026-05-21 (7972e7b829) when examples/app-crm declared `\"test:e2e\": \"playwright test\"` beside an e2e/ dir and a playwright.config.ts, and e737fbce39 ('simplify app-crm to minimal metadata smoke-test', 2026-05-24) deleted that script, leaving the turbo entry behind — so it is residue of a deleted script whose intent (configure the repo's Playwright suite) is still live, not a mistaken entry to delete. The rename went to the task key rather than the script because test:smoke has four live callers (showcase-smoke.yml, two platform-checklist area files, a vitest.config.ts comment) and test:e2e has zero. Half 2: check-turbo-task-graph's verdict() now judges generic (#-less) keys — a generic key must be declared by >=1 workspace MEMBER, with the root-manifest near miss written into the failure text and pinned as text in --self-test, plus a second non-vacuity floor for an empty generic population. Two card claims were falsified and are reported rather than papered over: (a) `e2e/**` does NOT name a directory app-showcase lacks — examples/app-showcase/e2e/ is tracked on origin/main with four files and playwright.config.ts declares testDir: './e2e', so all three input globs resolve (108 files hashed in the dry run) and are left as they stand; (b) Zone 2b measured SAFE — no CI workflow invokes `turbo run test:e2e`, so nothing was silently running nothing in CI (showcase-smoke.yml calls `pnpm test:smoke` with working-directory examples/app-showcase, bypassing turbo entirely — filed as #12466). Zone 2c measured: the only dependsOn targets in the whole file are `build` and `^build`, and `build` is held by 72/78, so NO generic key exists only as a dependsOn target; no exemption was added, and the header states why one would be a hole. One glob dropped inside the declared surface: `playwright-report/**`, which showcase-smoke.yml's own comment measures as never created under this config's stdout-only reporters (receipt: run 30796529117). FILE SURFACE AMENDED — the root package.json script row was added under the bounded in-place exemption, declared in a claim amendment comment on this card with its four conditions; forced by measurement, since after the rename `turbo run test:e2e` exits 1 ('Could not find task test:e2e in project'), so leaving the root row would convert this card's silent exit-0 no-op into a hard error.",
      "tests": "All results at final commit c246e4fcf6 on a clean tree (git rev-parse --short HEAD), quoted from each gate's own printed verdict line, exit codes captured before any pipe. TURBO BEHAVIOUR, measured on turbo 2.10.10 via `--dry=json`: pre-fix `turbo run test:e2e` -> EXIT=0, 78 tasks placed, 0 with a real command (the card's silent green, confirmed at tool level); post-fix `turbo run test:smoke` -> exactly 1 real command, `@objectstack/example-showcase#test:smoke` = `playwright test --config=playwright.config.ts`, outputs ['test-results/**'], cache local:false remote:false, deps ['@objectstack/example-showcase#build'], 108 input files including e2e/showcase-smoke.spec.ts; post-fix `turbo run test:e2e` -> EXIT=1 'Could not find task test:e2e in project'. REVERSE VERIFICATION, both legs, mutation confirmed on disk by grepping the exact text meant to change ('\"test:e2e\"'=1, '\"test:smoke\"'=0) and restored under trap ... EXIT INT TERM: the WIDENED gate on the pre-fix turbo.json -> 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 PRE-WIDENING gate (origin/main's copy) on that same pre-fix tree -> EXIT=0, 'OK: 21 package-scoped turbo task(s) judged...' — which is why the residue survived three months. Restore proved by hash, not by an empty diff: git hash-object turbo.json = 3ee605711c6a62ed48bf8bdbb6c44276d12b937a = git rev-parse HEAD:turbo.json. ABLATION of the new limb: `holdersOf(key, scriptsByPackage).length === 0` mutated to `.length < 0`, confirmed on disk by counting both the removed and injected text ('=== 0': 1 -> 0; '< 0': 0 -> 1), then --self-test re-run -> EXIT=1, 4 named cases 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:), self-test green again. NO REBUILD LEG APPLIES: this gate is plain Node reading the repo from source (no package export/dist resolution, no vitest alias), so there is no stale-dist direction for either ablation leg — stated rather than skipped. GATE UNION re-derived at the final commit 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; stderr confirmed 'objectstack-ai/objectstack at commit c246e4fcf6'), all EXIT=0: check:turbo-task-graph --self-test ('OK: all cases passed'); check:turbo-task-graph ('OK: 21 package-scoped and 8 generic turbo task(s) judged against 78 workspace package(s)'); check:nul-bytes + --self-test (6892 text files, 75 assertions); check:pnpm-filter-targets (140/177 --filter occurrences across 30 files resolve); check:cross-package-test-inputs (18 packages read outside themselves, all declared); check:ci-filter-parity (105 declared cross-package globs covered); check:agent-test-spelling; check:bash32-floor (98 cases); check:cli-command-ids (280 literals across 100 files); check:entry-guard (170 scripts/ files); check:parse-guard (46 cases). Both convention-triggered obligations 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') and `check:pm-dispatch-gates` EXIT=0 (703 cases); ROOT_FILE_WATCH_HINTS is unchanged so no new bare-root verdict is incurred. LINT — a DECLARED NARROWING with all three measurements, not a skipped step: (1) population read from eslint's own config — it reports package.json and turbo.json as 'File ignored because no matching configuration was supplied'; (2) count from --format json — 3 files requested, 1 actually linted (scripts/check-turbo-task-graph.mjs), 0 errors 0 warnings; (3) invariance — eslint.config.mjs lines 326-335 record with a positive control that this repo 'never enables type-aware linting ... for ANY file', so this diff cannot move any untouched file's verdict. NO pnpm test / pnpm typecheck is owed: no package source changed (turbo.json, one root script row, one dependency-free script in scripts/). CI is not waited on per the 2026-08-10 ruling; PR check status at report time is in_progress.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12465: check:turbo-task-graph reports turbo's legitimate `//#lint` root-task spelling as an unknown package `//`, and its failure text asserts 'the override never reaches the task graph' — contradicted by a dry run showing the task in the graph with a real root command at exit 0; sits in the PRE-EXISTING package-scoped arm, untouched by this PR; filed as a sub-issue of #12046",
        "filed as #12466: the showcase smoke's only CI entry point bypasses turbo (showcase-smoke.yml uses working-directory + `pnpm test:smoke`), so the turbo.json task config repaired by this card has zero CI consumers today; observation, nothing red"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions