Work type
Bug / regression
Observed behavior
internal/fakeprovider.Turn has Delay and InterDeltaDelay fields, but the JSON loader the daemon actually reads (fakeProviderTurnJSON / loadFakeTurns in cmd/harnessd/main.go) exposes only content, deltas, tool_calls, usage, cost_usd, and cost_status. There is no way to set a delay from a turns file, so the fields are unreachable in any end-to-end test that drives a real daemon.
Consequence: a scripted turn's reasoning delta, content delta, and tool call all land inside a single sub-millisecond Complete() call. Any UI state that exists only between those events is unobservable.
Found while verifying #1415. The TUI spinner's Thinking and Writing response states could not be captured in a live pty run — they exist for microseconds, while the spinner label refreshes on a 120ms tick (cmd/harnesscli/tui/animation.go, SpinnerInterval). Running <tool> captured reliably, because it is gated by real tool wall-clock time (sleep 3) rather than provider timing. Waiting for model appeared only once, by winning a sub-tick race.
Expected behavior
A turns file can specify a per-turn delay and an inter-delta delay, so a test can hold the daemon in a given streaming state long enough for a client to observe it. The existing Delay and InterDeltaDelay fields already model this; the loader simply does not carry them.
Reproduction
- Write a turns file with a reasoning delta, a content delta, and a tool call.
- Start
harnessd with HARNESS_PROVIDER=fake and HARNESS_FAKE_TURNS pointing at it.
- Drive
harnesscli --tui through a pty and sample its output.
Actual: the run jumps from the initial state to the tool-running state. The intermediate streaming states never render.
User and operational impact
No end-user impact — this is test infrastructure. The cost is that TUI and client behavior which depends on streaming timing cannot be proven end to end, so changes to it ship on unit-test evidence alone. That is exactly the gap that let a 40-column truncation bug in #1415 reach a rendered frame before anyone noticed it, and it will keep any similar timing-dependent regression invisible to the acceptance suites.
Suspected seam and search evidence
Owning code: cmd/harnessd/main.go, the fakeProviderTurnJSON struct and loadFakeTurns function. The struct is the wire format for HARNESS_FAKE_TURNS; it needs two more optional fields mapped onto fakeprovider.Turn.Delay and .InterDeltaDelay.
Consumers of the turns format that must keep working: scripts/run-bench-smoke.sh, scripts/smoke-test.sh, internal/acceptance/ptyrunner, and cmd/native-gui-acceptance. All write turns files without a delay field, so the addition has to be optional and default to zero.
Blast-radius impact map
Callers and data flow: the turns-file loader only. No provider-facing or protocol change.
Config/env/defaults: HARNESS_FAKE_TURNS gains two optional keys. Absent keys mean zero delay, which is today's behavior.
API/CLI/wire formats/tools: none beyond the turns-file schema, which is test-only.
Persistence/schema/cache: none.
Concurrency/lifecycle: a delay makes fake completions slower on purpose. Any existing test that assumes a near-instant fake turn is unaffected as long as the default stays zero.
Security/auth/permissions/privacy: none.
Provider/model/tool catalog: none — the fake provider is not in the catalog.
Deployment/observability/runbooks: docs/runbooks/benchmark-smoke.md and any runbook documenting the turns-file schema.
Compatibility: additive and optional.
Existing tests/fixtures: every turns file in the tree; none set the new fields, so all keep working.
Regression test first
A loader test asserting a turns file with "delay_ms" and "inter_delta_delay_ms" produces a fakeprovider.Turn with those durations set, and that a file omitting them yields zero. Red before the change, since the fields cannot round-trip today.
Fix boundaries
In scope: the two optional fields on fakeProviderTurnJSON, their mapping in loadFakeTurns, a loader test, and a docs note on the turns-file schema.
Out of scope: the TUI spinner itself (#1415, merged separately); any change to internal/fakeprovider.Turn, which already has the fields; adding delays to existing turns files.
Verification plan
Rollout and rollback
Test-only change; no deployment concern. Rollback is reverting the commit.
Documentation and handoff
docs/runbooks/benchmark-smoke.md (or wherever the turns-file schema is documented) plus an engineering-log entry noting why the gap mattered.
Definition of done
Work type
Bug / regression
Observed behavior
internal/fakeprovider.TurnhasDelayandInterDeltaDelayfields, but the JSON loader the daemon actually reads (fakeProviderTurnJSON/loadFakeTurnsincmd/harnessd/main.go) exposes onlycontent,deltas,tool_calls,usage,cost_usd, andcost_status. There is no way to set a delay from a turns file, so the fields are unreachable in any end-to-end test that drives a real daemon.Consequence: a scripted turn's reasoning delta, content delta, and tool call all land inside a single sub-millisecond
Complete()call. Any UI state that exists only between those events is unobservable.Found while verifying #1415. The TUI spinner's
ThinkingandWriting responsestates could not be captured in a live pty run — they exist for microseconds, while the spinner label refreshes on a 120ms tick (cmd/harnesscli/tui/animation.go,SpinnerInterval).Running <tool>captured reliably, because it is gated by real tool wall-clock time (sleep 3) rather than provider timing.Waiting for modelappeared only once, by winning a sub-tick race.Expected behavior
A turns file can specify a per-turn delay and an inter-delta delay, so a test can hold the daemon in a given streaming state long enough for a client to observe it. The existing
DelayandInterDeltaDelayfields already model this; the loader simply does not carry them.Reproduction
harnessdwithHARNESS_PROVIDER=fakeandHARNESS_FAKE_TURNSpointing at it.harnesscli --tuithrough a pty and sample its output.Actual: the run jumps from the initial state to the tool-running state. The intermediate streaming states never render.
User and operational impact
No end-user impact — this is test infrastructure. The cost is that TUI and client behavior which depends on streaming timing cannot be proven end to end, so changes to it ship on unit-test evidence alone. That is exactly the gap that let a 40-column truncation bug in #1415 reach a rendered frame before anyone noticed it, and it will keep any similar timing-dependent regression invisible to the acceptance suites.
Suspected seam and search evidence
Owning code:
cmd/harnessd/main.go, thefakeProviderTurnJSONstruct andloadFakeTurnsfunction. The struct is the wire format forHARNESS_FAKE_TURNS; it needs two more optional fields mapped ontofakeprovider.Turn.Delayand.InterDeltaDelay.Consumers of the turns format that must keep working:
scripts/run-bench-smoke.sh,scripts/smoke-test.sh,internal/acceptance/ptyrunner, andcmd/native-gui-acceptance. All write turns files without a delay field, so the addition has to be optional and default to zero.Blast-radius impact map
Callers and data flow: the turns-file loader only. No provider-facing or protocol change.
Config/env/defaults:
HARNESS_FAKE_TURNSgains two optional keys. Absent keys mean zero delay, which is today's behavior.API/CLI/wire formats/tools: none beyond the turns-file schema, which is test-only.
Persistence/schema/cache: none.
Concurrency/lifecycle: a delay makes fake completions slower on purpose. Any existing test that assumes a near-instant fake turn is unaffected as long as the default stays zero.
Security/auth/permissions/privacy: none.
Provider/model/tool catalog: none — the fake provider is not in the catalog.
Deployment/observability/runbooks:
docs/runbooks/benchmark-smoke.mdand any runbook documenting the turns-file schema.Compatibility: additive and optional.
Existing tests/fixtures: every turns file in the tree; none set the new fields, so all keep working.
Regression test first
A loader test asserting a turns file with
"delay_ms"and"inter_delta_delay_ms"produces afakeprovider.Turnwith those durations set, and that a file omitting them yields zero. Red before the change, since the fields cannot round-trip today.Fix boundaries
In scope: the two optional fields on
fakeProviderTurnJSON, their mapping inloadFakeTurns, a loader test, and a docs note on the turns-file schema.Out of scope: the TUI spinner itself (#1415, merged separately); any change to
internal/fakeprovider.Turn, which already has the fields; adding delays to existing turns files.Verification plan
go test ./cmd/harnessd/... -race.bash scripts/run-bench-smoke.shto prove existing delay-free turns files are unaffected.ThinkingandWriting responserender as distinct spinner frames. That capture is the reason this issue exists, so it is the check that matters.Rollout and rollback
Test-only change; no deployment concern. Rollback is reverting the commit.
Documentation and handoff
docs/runbooks/benchmark-smoke.md(or wherever the turns-file schema is documented) plus an engineering-log entry noting why the gap mattered.Definition of done
ThinkingandWriting responseobserved as distinct frames in a pty capture