f4f1efcb6f
Detect OpenCode promptAsync calls that return before a child session has any durable message, and surface a prompt acceptance error before the generic five-minute sync poll timeout. Add a failing-first regression for the idle zero-message case and keep the existing durable-message completion path covered. Debugging-Journal: .debugging
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import { describe, expect, mock, test } from "bun:test"
|
|
|
|
import { waitForCompletion } from "./completion-poller"
|
|
|
|
function createToolContext(): Parameters<typeof waitForCompletion>[1] {
|
|
return {
|
|
sessionID: "parent-session",
|
|
messageID: "parent-message",
|
|
agent: "sisyphus",
|
|
abort: new AbortController().signal,
|
|
metadata: mock(() => {}),
|
|
}
|
|
}
|
|
|
|
function createContext(args: {
|
|
status: ReturnType<typeof mock>
|
|
messages: ReturnType<typeof mock>
|
|
}): Parameters<typeof waitForCompletion>[2] {
|
|
return {
|
|
client: {
|
|
session: {
|
|
status: args.status,
|
|
messages: args.messages,
|
|
},
|
|
},
|
|
} as never
|
|
}
|
|
|
|
describe("waitForCompletion", () => {
|
|
test("#given promptAsync returned before OpenCode saved a user message #when the child session stays idle with zero messages #then it fails as a prompt acceptance error", async () => {
|
|
// given
|
|
const originalDateNow = Date.now
|
|
const originalSetTimeout = globalThis.setTimeout
|
|
let currentTime = 0
|
|
Date.now = () => {
|
|
currentTime += 60_000
|
|
return currentTime
|
|
}
|
|
globalThis.setTimeout = ((handler: TimerHandler) => {
|
|
if (typeof handler === "function") {
|
|
handler()
|
|
}
|
|
return originalSetTimeout(() => {}, 0)
|
|
}) as typeof globalThis.setTimeout
|
|
|
|
const status = mock(async () => ({ data: { "ses-undurable": { type: "idle" } } }))
|
|
const messages = mock(async () => ({ data: [] }))
|
|
|
|
try {
|
|
// when
|
|
const result = waitForCompletion(
|
|
"ses-undurable",
|
|
createToolContext(),
|
|
createContext({ status, messages }),
|
|
)
|
|
|
|
// then
|
|
await expect(result).rejects.toThrow("Prompt was not durably accepted by OpenCode")
|
|
expect(messages).toHaveBeenCalled()
|
|
} finally {
|
|
Date.now = originalDateNow
|
|
globalThis.setTimeout = originalSetTimeout
|
|
}
|
|
})
|
|
|
|
test("#given the child session has durable messages #when it stays idle and stable #then completion succeeds", async () => {
|
|
// given
|
|
const originalDateNow = Date.now
|
|
const originalSetTimeout = globalThis.setTimeout
|
|
let currentTime = 0
|
|
Date.now = () => {
|
|
currentTime += 100
|
|
return currentTime
|
|
}
|
|
globalThis.setTimeout = ((handler: TimerHandler) => {
|
|
if (typeof handler === "function") {
|
|
handler()
|
|
}
|
|
return originalSetTimeout(() => {}, 0)
|
|
}) as typeof globalThis.setTimeout
|
|
|
|
const status = mock(async () => ({ data: { "ses-complete": { type: "idle" } } }))
|
|
const messages = mock(async () => ({
|
|
data: [
|
|
{ info: { id: "msg-user", role: "user" } },
|
|
{ info: { id: "msg-assistant", role: "assistant" } },
|
|
],
|
|
}))
|
|
|
|
try {
|
|
// when
|
|
await waitForCompletion(
|
|
"ses-complete",
|
|
createToolContext(),
|
|
createContext({ status, messages }),
|
|
)
|
|
|
|
// then
|
|
expect(messages).toHaveBeenCalled()
|
|
} finally {
|
|
Date.now = originalDateNow
|
|
globalThis.setTimeout = originalSetTimeout
|
|
}
|
|
})
|
|
})
|