Repository navigation
TestQuestionsE2E has been 14/18 red since #933: the suite's inline needles still spell the pre-#933 key grammar #998
Description
What happened
A full live run of TestQuestionsE2E on 2026-09-12 against fix/permission-frame-safe-answer (b7e641ad5, a branch off dev@82624af79 touching only the permission frame's safe answer mark) came back 14 of 18 subtests red, on deepseek/deepseek-v4-flash, 1414 s, on the Spark:
--- FAIL: TestQuestionsE2E (1414.47s)
--- FAIL: TestQuestionsE2E/ALineAsksAndTheKeysAnswerIt (211.88s)
--- FAIL: TestQuestionsE2E/ACardCountsDownToItsPickAndEnterTakesIt (8.19s)
--- FAIL: TestQuestionsE2E/TheRoomComparesAnnotatesAsksBackAndSends (375.92s)
--- FAIL: TestQuestionsE2E/ACardWithABlockUnderEachAnswerArrivesWhole (35.97s)
--- FAIL: TestQuestionsE2E/ASentenceWithHolesIsFilledIn (33.91s)
--- FAIL: TestQuestionsE2E/AChecklistTicksSeveralAnswers (34.89s)
--- FAIL: TestQuestionsE2E/PairsAreAnsweredOneRowAtATime (29.13s)
--- FAIL: TestQuestionsE2E/ADialIsMovedWithTheArrows (30.63s)
--- FAIL: TestQuestionsE2E/ARatifiedActSaysWhatItDidAndHowToUndoIt (4.89s)
--- FAIL: TestQuestionsE2E/AssumptionsStandUntilOneIsStruck (7.17s)
--- PASS: TestQuestionsE2E/TwoQuestionsInOneStepAreOnePanelWithTabs (18.77s)
--- PASS: TestQuestionsE2E/FourReadsInOneBatchAreOnePermissionFrame (17.48s)
--- FAIL: TestQuestionsE2E/AnotherWindowAnswersAndTheFirstSaysWho (199.56s)
--- FAIL: TestQuestionsE2E/AQuestionWhoseSubjectWentAwayIsWithdrawn (187.21s)
--- PASS: TestQuestionsE2E/AProjectRuleDecidesAChoiceAndTheRowWearsIt (13.42s)
--- PASS: TestQuestionsE2E/HeadlessTakesTheDefaultAndSaysSo (6.65s)
--- FAIL: TestQuestionsE2E/TheOrdinaryRoadCarriesAQuestionAndItsAnswer (194.71s)
--- FAIL: TestQuestionsE2E/TheOlderBlocksHaveNotMovedOntoTheObject (4.07s)
The model did nothing wrong and the surface drew the question correctly. The first failure waited three minutes for a screen that was already there:
questions_e2e_test.go:325: waited 3m0s for ["delete the build directory?" "[1] delete it"] and never saw it. the screen was:
…
─ stale build directory delet… · deepseek/deepseek-v4-flash · ⠿ auto · via wafer ─── esc interrupt ─
? delete the build directory? ▸1 delete it 2 leave it
the build directory is stale
←→ choose · enter take it · esc later · c change · ? ask back · waiting
The surface draws ▸1 delete it. The suite waits for [1] delete it. It has been waiting for it since #933 (23f5da1eb, "Questions: one frame, one chooser, one key grammar — and the amber on the marks alone"), which is the commit that removed the bracketed key spelling:
$ git log --oneline -S'"["+key+"]"' -- internal/tui3/question.go
23f5da1eb Questions: one frame, one chooser, one key grammar — and the amber on the marks alone (#933)
c7dbb13ba Questions: the block — one renderer for the line, the card and the ratify row (#734)
$ git show origin/dev:internal/tui3/question.go | grep -c '\[" *+ *key'
0Why the guard did not catch it
internal/e2e/tuiwords_test.go exists precisely so this cannot happen, and an untagged test reads its needles back against internal/tui3's sources — it is what #184 bought. But the rotted needles are inline string literals in the test body, not table entries:
screen := awaitQuestion(t, r, "delete the build directory?", "[1] delete it")
screenSays(t, screen, "[2] leave it", "the line's second answer")
screenSays(t, screen, say(t, "questionLaterKeyWord"), "the line's way out")The third line is guarded. The first two are not, and go test ./internal/e2e/ is green with them. So the table's promise — "fails the moment the surface stops spelling a sentence the suite waits for" — is only as good as the share of needles that go through say, and nothing enforces that share.
That is the same shape as #184 (silent rot for a week) and #576 (a suite reporting green without running): a gate that looks complete and has a hole in it.
Replication
Deterministic (no model, no key, sub-second). From a clean dev:
grep -n '"\[[0-9]\] ' internal/e2e/questions_e2e_test.go # the stale needles, still waited for
git show origin/dev:internal/tui3/question.go | grep -c '"\[" *+ *key' # 0 — nothing spells them
go test ./internal/e2e/ # ok — the guard passes anywayThe wrong output is that last ok: the untagged gate is green while the suite it guards cannot pass.
Field (real models). make test-e2e or:
go test -tags e2e -run 'TestQuestionsE2E/ALineAsksAndTheKeysAnswerIt' -count=1 -timeout 40m -v ./internal/e2e/Needs OPENROUTER_API_KEY (resolved via liveKey), tmux, and make build first. About four minutes and a fraction of a cent for that one subtest; the whole of TestQuestionsE2E is ~24 minutes and roughly a dollar. It fires on every run — there is nothing flaky about it.
Where
questionsLine,questionsCard,questionsRoom,questionsBlanks,questionsChecklist,questionsPairs,questionsDial,questionsRatify,questionsAssumption,questionsReach,questionsWithdrawn,questionsDefaultDoor,questionsOlderBlocksininternal/e2e/questions_e2e_test.go— search for"[1]and"[2].- The needle table and its untagged guard:
internal/e2e/tuiwords_test.go; the guard isTestEveryLaneAsksForItsKeyTheWayTheProductDoes's neighbour in the same package. - The spelling the surface actually uses:
questionAnswerWordandquestionRowKeysininternal/tui3/question.go.
The fix
Two things, and the second is the one that matters:
- Re-read each rotted needle against the surface and move it into the
tuiwords_test.gotable with awhyline, so it is spelled once. The line form's answers read▸1 delete it/2 leave ittoday — the pointer mark isGPointerthrough the glyph vocabulary, so the needle must be built from the token, not pasted from a screenshot, or it will rot again on an ASCII terminal. - Close the hole. Add an untagged law that fails the build on a screen-text literal passed to
awaitQuestion/screenSaysfrom anywhere in the package — ago/astwalk asserting those arguments aresay(t, …)calls or identifiers, never*ast.BasicLit. Without it the table is a convention, and this issue will recur the next time a person-facing string moves.
Acceptance
- e2e:
go test -tags e2e -run 'TestQuestionsE2E' -count=1 -timeout 60m ./internal/e2e/exits 0 with 18/18 subtests passing ondeepseek/deepseek-v4-flash. - e2e: the control —
TestQuestionsE2E/FourReadsInOneBatchAreOnePermissionFrameandTwoQuestionsInOneStepAreOnePanelWithTabs, which pass today, still pass; they are the two lanes whose needles already go throughsay, and they are why this is rot and not an outage. - Unit: the new untagged law fails on a deliberately re-inlined literal (
screenSays(t, screen, "[1] delete it", …)) and passes on the table form, andgo test ./internal/e2e/still runs in under a second. - The change entry's
invalidatesrecords thatTestQuestionsE2Ewas not a working gate between Questions: one frame, one chooser, one key grammar — and the amber on the marks alone #933 and this fix, so nobody reads a green CI line from that window as proof of anything.
Found on 2026-09-12 while verifying #992 on a real model. Not caused by that branch — it touches only the permission frame's safe answer mark, and its two frame subtests are among the four that pass. Filed rather than fixed there because the fix is a separate piece of work that needs its own paid run.
Drafted with CodeAF · reviewed and owned by the author
Activity