fix(cli): emit run events once

This commit is contained in:
Josh Lambert
2026-07-23 23:09:42 -04:00
parent bdb9070a9e
commit bcf8b8b9a8
9 changed files with 88 additions and 95 deletions
@@ -74,7 +74,7 @@ describe("opencode run (non-interactive subprocess)", () => {
({ llm, opencode }) =>
Effect.gen(function* () {
yield* llm.text("structured output")
const result = yield* opencode.run("say hi", { format: "json" })
const result = yield* opencode.run("say hi", { format: "json", extraArgs: ["--auto"] })
opencode.expectExit(result, 0)
const events = opencode.parseJsonEvents(result.stdout)
@@ -83,9 +83,25 @@ describe("opencode run (non-interactive subprocess)", () => {
expect(typeof evt.type).toBe("string")
expect(typeof evt.sessionID).toBe("string")
}
// At least one `text` event should appear with the LLM's response.
const text = events.find((e) => e.type === "text")
expect(text).toBeDefined()
expect(events.filter((event) => event.type === "step_start")).toHaveLength(1)
expect(events.filter((event) => event.type === "text")).toHaveLength(1)
expect(events.filter((event) => event.type === "step_finish")).toHaveLength(1)
}),
60_000,
)
cliIt.live(
"--format json emits each completed tool once",
({ llm, opencode }) =>
Effect.gen(function* () {
yield* llm.tool("glob", { pattern: "package.json" })
yield* llm.text("tool complete")
const result = yield* opencode.run("find package.json", { format: "json", extraArgs: ["--auto"] })
opencode.expectExit(result, 0)
const events = opencode.parseJsonEvents(result.stdout)
expect(events.filter((event) => event.type === "tool_use")).toHaveLength(1)
expect(events.filter((event) => event.type === "text")).toHaveLength(1)
}),
60_000,
)
@@ -88,20 +88,14 @@ function retry(sessionID: string, attempt: number, message: string) {
function assistant(id: string, sessionID = "session-1"): SdkEvent {
return {
id: `evt-${id}`,
type: "sync",
syncEvent: {
type: "message.updated.1",
id: `evt-${id}`,
seq: 1,
aggregateID: sessionID,
data: {
type: "message.updated",
properties: {
sessionID,
info: assistantMessage({
sessionID,
info: assistantMessage({
sessionID,
id,
parts: [],
}).info,
},
id,
parts: [],
}).info,
},
}
}
@@ -295,6 +289,18 @@ function textPart(id: string, messageID: string, text: string, sessionID = "sess
}
function textUpdated(part: TextPart): SdkEvent {
return {
id: `evt-${part.id}-updated`,
type: "message.part.updated",
properties: {
sessionID: part.sessionID,
part,
time: 1,
},
}
}
function syncTextUpdated(part: TextPart): SdkEvent {
return {
id: `evt-${part.id}-updated`,
type: "sync",
@@ -338,17 +344,11 @@ function reasoningUpdated(part: ReasoningPart): SdkEvent {
function toolUpdated(part: SessionToolPart): SdkEvent {
return {
id: `evt-${part.id}-updated`,
type: "sync",
syncEvent: {
type: "message.part.updated.1",
id: `evt-${part.id}-updated`,
seq: 1,
aggregateID: part.sessionID,
data: {
sessionID: part.sessionID,
part,
time: 1,
},
type: "message.part.updated",
properties: {
sessionID: part.sessionID,
part,
time: 1,
},
}
}
@@ -468,6 +468,34 @@ function sdk(
}
describe("run stream transport", () => {
test("ignores the sync copy of a native message event", async () => {
const src = globalFeed()
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({ globalStream: src.stream }),
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
})
const part = {
...textPart("text-1", "msg-1", "Hello"),
time: { start: 1, end: 2 },
}
try {
src.push(globalEvent(assistant("msg-1")))
src.push(globalEvent(textUpdated(part)))
src.push(globalEvent(syncTextUpdated(part)))
await waitFor(() => ui.commits.find((item) => item.kind === "assistant" && item.text === "Hello"))
expect(ui.commits.filter((item) => item.kind === "assistant" && item.text === "Hello")).toHaveLength(1)
} finally {
src.close()
await transport.close()
}
})
test("does not replay persisted main-session history during bootstrap by default", async () => {
const src = eventFeed()
const ui = footer()