Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton bd64eb4649 test(opencode/run): skip flaky scrollback replay test on Windows
The "renders replayed user, reasoning, and assistant output after
completion" test reliably fails on Windows CI while passing on Linux
and macOS, blocking unrelated PRs behind branch protection.

The failure is in the streaming CodeRenderable path: a reasoning commit
becomes a `<code filetype="markdown" streaming drawUnstyledText={false}>`
renderable, whose `set content` short-circuits the textBuffer update
during streaming and relies on the next highlight cycle to populate it.
On Windows the first paragraph (`_Thinking:_ **Plan**`) is missing from
the committed rows after `settle()`; on Linux (where `useThread` is
forced off in `@opentui/core/testing`) and macOS the rows commit
correctly.

Skipping the case on `win32` with an inline investigation summary so the
next person can pick this up without redoing the trace. The assertion
still runs on Linux and macOS CI.
2026-05-18 19:23:45 -04:00
@@ -403,38 +403,82 @@ test("inserts spacers for new visible groups", async () => {
}
})
test("renders replayed user, reasoning, and assistant output after completion", async () => {
const out = await setup()
// TODO(windows): Re-enable on Windows once the streaming CodeRenderable
// flush race is fixed. The reasoning commit is delivered as a `<code>`
// renderable with `filetype="markdown"`, `streaming=true`, and
// `drawUnstyledText=false`. On Windows the first paragraph of the reasoning
// body (here `_Thinking:_ **Plan**`) is dropped from the committed rows —
// the failing assertion shows only `Say hello.` survives, while Linux
// (where `useThread` is forced off in `@opentui/core/testing`) and macOS
// both pass.
//
// Investigation summary (see PR description for the link to this work):
// 1. `reasoning("Thinking: ...", "progress")` enters `entry.body.ts`
// `reasoningBody`, which becomes a `code` body with filetype="markdown".
// 2. `RunScrollbackStream.writeStreaming` sets `renderable.content = ...`
// while `streaming=true`. `CodeRenderable.set content` short-circuits
// (does NOT call `textBuffer.setText`) when streaming, drawUnstyledText
// is false, and a filetype is set — it relies on the next
// `startHighlight()` cycle to populate the buffer.
// 3. `ScrollbackSurface.settle()` renders the surface, kicks the
// highlight via `renderSelf` → `startHighlight`, waits on
// `highlightingDone`, and re-renders. With `MockTreeSitterClient`
// returning `{highlights: []}`, the final branch (`else
// this.textBuffer.setText(content)`) populates the buffer and
// `_shouldRenderTextBuffer = true`.
// 4. `flushActive` then commits rows `[0, surface.height - 1)` during
// streaming. On Windows the committed rows are blank for the first
// paragraph — suggesting the height/text-buffer state is observed
// before/after the highlight resolution in a way that drops rows on
// that platform.
//
// The Linux pass path takes `useThread = false` (see
// `@opentui/core/testing.js` line ~540) which serializes the FFI render
// thread. macOS passes despite `useThread = true`, so the divergence is
// likely either Bun's microtask scheduling on Windows or a Zig-side
// threading interaction during the second `renderSurface()` pass in
// `settleSurface`. A real fix probably belongs in opentui (either force
// `useThread=false` for testing on Windows, or eagerly call
// `textBuffer.setText` in `CodeRenderable.set content` when streaming
// updates a non-empty body).
//
// Skipping on win32 unblocks unrelated PRs; the assertion is still
// exercised on Linux and macOS in CI.
test.skipIf(process.platform === "win32")(
"renders replayed user, reasoning, and assistant output after completion",
async () => {
const out = await setup()
try {
const lines: string[] = []
const take = () => {
const commits = claim(out.renderer)
try {
lines.push(...commits.flatMap((commit) => renderRows(commit).flatMap((row) => row.split("\n"))))
} finally {
destroy(commits)
try {
const lines: string[] = []
const take = () => {
const commits = claim(out.renderer)
try {
lines.push(...commits.flatMap((commit) => renderRows(commit).flatMap((row) => row.split("\n"))))
} finally {
destroy(commits)
}
}
await out.scrollback.append(user("Hello you"))
take()
await out.scrollback.append(reasoning("Thinking: **Plan**\n\nSay hello.", "progress"))
await out.scrollback.complete()
take()
await out.scrollback.append(assistant("Hello.", "progress"))
await out.scrollback.complete()
take()
const output = lines.join("\n")
expect(output).toContain(" Hello you")
expect(output).toContain("Thinking:")
expect(output).toContain("Plan")
expect(output).toContain("Hello.")
} finally {
out.scrollback.destroy()
}
await out.scrollback.append(user("Hello you"))
take()
await out.scrollback.append(reasoning("Thinking: **Plan**\n\nSay hello.", "progress"))
await out.scrollback.complete()
take()
await out.scrollback.append(assistant("Hello.", "progress"))
await out.scrollback.complete()
take()
const output = lines.join("\n")
expect(output).toContain(" Hello you")
expect(output).toContain("Thinking:")
expect(output).toContain("Plan")
expect(output).toContain("Hello.")
} finally {
out.scrollback.destroy()
}
})
},
)
test("coalesces same-line tool progress into one snapshot", async () => {
const out = await setup()