Repository navigation
[Hackathon] fix(test): test lint passes plans that test create / create-batch reject, and fails ones they accept #374
Description
Verified against (commit SHA or released version)
commit 988c0355bb05631e9c6c1785e443937ff2a88c0e (main, v0.14.0)
Discord Username / User ID
1149447580477042769
What does this improvement do?
test lint promises (in --help) to "Validate plan/steps files offline with the same validators create runs". DOCUMENTATION.md places it in the authoring loop as the batch counterpart of test create --plan-from --dry-run. #98 accepted it on the condition that it reuse the create guards. Its directory reader and its type check, however, were written separately and have drifted from test create / test create-batch. Lint now gives the wrong answer in both directions, so a pre-commit hook or a CI step that runs test lint before create-batch can't be trusted.
False green: lint passes, create refuses.
- A
"type": "backend"plan.- Lint reports
1/1 validand exits 0. Its error text even liststype: must be one of: "frontend", "backend". test create --plan-fromrejects the same file locally with exit 5: "Backend tests via the CLI require --code-file."create-batchwarns "server will return per-spec validation_error for these" and the spec fails.schemas/plan.schema.jsonallows only"enum": ["frontend"].
- Lint reports
- A
--plansJSONL with 51 specs. Lint reports51/51 valid, exit 0.create-batchrefuses it: "must contain at most 50 specs (got 51)", exit 5. The same 50-spec cap applies to--plan-from-dir.
False red / unchecked file: lint and create-batch read a directory differently.
- Lint matches
*.jsoncase-sensitively, soB.JSONis never linted, yetcreate-batchingests it. - Lint reports a non-plan JSON file in the directory (e.g.
suite-index.json, which has noprojectId/planSteps) as 4 errors and exits 5.create-batchdeliberately skips it with[warn] Skipping suite-index.json: not a plan fileand succeeds.
Reproduction
mkdir -p lintrepro/plandir && cd lintrepro
echo '{"projectId":"prj_1","type":"backend","name":"Health","planSteps":[{"type":"action","description":"GET /health"}]}' > be.plan.json
echo '{"projectId":"prj_1","type":"frontend","name":"Login","planSteps":[{"type":"action","description":"Open /login"}]}' > plandir/a.json
sed 's/Login/Login B/' plandir/a.json > plandir/B.JSON
echo '{"suite":"checkout","owner":"qa"}' > plandir/suite-index.json
for i in $(seq 1 51); do echo "{\"projectId\":\"prj_1\",\"type\":\"frontend\",\"name\":\"T$i\",\"planSteps\":[{\"type\":\"action\",\"description\":\"Open /\"}]}"; done > fiftyone.jsonl
testsprite test lint --plan-from be.plan.json; echo "exit=$?" # 1/1 valid, exit=0
testsprite test create --plan-from be.plan.json --dry-run >/dev/null; echo "exit=$?" # exit=5 (backend refused)
testsprite test lint --plans fiftyone.jsonl; echo "exit=$?" # 51/51 valid, exit=0
testsprite test create-batch --plans fiftyone.jsonl --dry-run >/dev/null; echo "exit=$?" # exit=5 (cap)
testsprite test lint --plan-from-dir plandir; echo "exit=$?" # 4 problems in suite-index.json, B.JSON not checked, exit=5
testsprite test create-batch --plan-from-dir plandir --dry-run; echo "exit=$?" # reads 3 files, skips suite-index.json, creates 2, exit=0Expected. For any input, lint's verdict matches what the create path would do with it: anything create / create-batch refuses locally (or that is certain to fail as a backend spec) is a lint issue, and anything they accept or deliberately skip is not.
Details / implementation notes
Root cause (src/commands/test.ts):
collectPlanIssues(lint) checksrequireEnum(${prefix}type, obj.type, ['frontend', 'backend']). The backend refusal lives only inrunCreateFromPlan(if (plan.type === 'backend') { … 'Backend tests via the CLI require --code-file.'), and the batch path only warns.runLint's--plan-from-dirbranch filters with.filter(name => name.endsWith('.json')).readPlansFromDirGuardeduses.filter(f => extname(f).toLowerCase() === '.json')and applies thelooksLikePlanskip heuristic, which lint lacks.- The
MAX_BATCH_SPECS(50) caps inreadPlansJsonlGuarded/readPlansFromDirGuardedhave no counterpart inrunLint.
Proposed change (one concern: lint ↔ create parity; create behaviour unchanged):
collectPlanIssuesreportstype: "backend"as an issue, using the same reasontest create --plan-fromgives and pointing attest create --type backend --code-file.- Factor the directory filter and the
looksLikePlanheuristic out ofreadPlansFromDirGuardedinto two small helpers that both readers call, so they cannot drift again. Lint then checks*.JSON, and it skips non-plan files with the same[warn] Skipping …line on stderr, leaving the JSON report shape unchanged. - Lint reports a
plans/plan-from-dirissue when the valid spec count exceedsMAX_BATCH_SPECS, as the create readers do. The 5 MB body cap can come along too if you want it. - Tests in the existing
runLintblock ofsrc/commands/test.test.ts, one per drift above. Each runs lint and the create reader on the same fixture and asserts they agree, and each fails onmaintoday.
I'd like to work on this once it's triaged.
Confirmations
- I have searched existing issues and this is not a duplicate.
- I have provided my Discord identity above for reward coordination.
Activity
protonjls commentedon Oct 22, 2026
Re-checked on current
main(a18d37c, v0.15.0; nothing undersrc/has changed sinced61e6df). The reproduction above gives the same results on a fresh build:test lintpasses the backend plan (exit 0).test create --plan-fromrefuses it (exit 5).test lintpasses the 51-spec JSONL (exit 0).test create-batch --plansrefuses it: "must contain at most 50 specs (got 51)" (exit 5).test lint --plan-from-dirreads 2 of the 3 files (1/2 valid, soB.JSONis never checked) and reportssuite-index.jsonas 4 problems (exit 5).test create-batch --plan-from-dirreads all 3, skipssuite-index.jsonas "not a plan file", and exits 0.If all four is more than you want, a smaller scope would be the backend
typecheck plus one directory filter andlooksLikePlanskip shared by both readers, leaving out the 50-spec cap. Iftest lintis changing or going away in 1.0, liketest flakyandtest diff, feel free to close this.I won't be opening a PR for this one myself, so once it's accepted it's open to anyone (after the 48-hour opener window).