Skip to content

test(fakeprovider): turns file cannot inject a delay, so fast TUI states are unobservable #1419

Description

@dennisonbertram

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

  1. Write a turns file with a reasoning delta, a content delta, and a tool call.
  2. Start harnessd with HARNESS_PROVIDER=fake and HARNESS_FAKE_TURNS pointing at it.
  3. 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

  • Loader test written first and observed failing
  • Both fields optional, defaulting to zero
  • Existing turns files and smoke scripts unaffected
  • Thinking and Writing response observed as distinct frames in a pty capture
  • Turns-file schema documented

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions