test(ci): stabilize cancellation and crash checkpoints
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"op": "promptAndCancel",
|
"op": "promptAndCancel",
|
||||||
"text": "Run two shell commands: wait for cancellation, then write skipped.txt.",
|
"text": "Run two shell commands: wait for cancellation, then write skipped.txt.",
|
||||||
"afterUpdate": "tool_call",
|
"afterUpdate": "tool_call",
|
||||||
|
"waitForFile": { "path": "started.txt" },
|
||||||
"waitForToolCallUpdate": "call_skipped"
|
"waitForToolCallUpdate": "call_skipped"
|
||||||
},
|
},
|
||||||
{ "op": "waitForTurnEnd" }
|
{ "op": "waitForTurnEnd" }
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"kind": "chunks",
|
"kind": "chunks",
|
||||||
"chunks": [
|
"chunks": [
|
||||||
{ "type": "block-start", "index": 0, "blockType": "tool-call" },
|
{ "type": "block-start", "index": 0, "blockType": "tool-call" },
|
||||||
{ "type": "tool-call-delta", "index": 0, "id": "call_wait", "name": "bash", "argumentsDelta": "{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" },
|
{ "type": "tool-call-delta", "index": 0, "id": "call_wait", "name": "bash", "argumentsDelta": "{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" },
|
||||||
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_wait", "name": "bash", "arguments": "{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" } },
|
{ "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_wait", "name": "bash", "arguments": "{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}" } },
|
||||||
{ "type": "block-start", "index": 1, "blockType": "tool-call" },
|
{ "type": "block-start", "index": 1, "blockType": "tool-call" },
|
||||||
{ "type": "tool-call-delta", "index": 1, "id": "call_skipped", "name": "bash", "argumentsDelta": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" },
|
{ "type": "tool-call-delta", "index": 1, "id": "call_skipped", "name": "bash", "argumentsDelta": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" },
|
||||||
{ "type": "block-end", "index": 1, "block": { "type": "tool-call", "id": "call_skipped", "name": "bash", "arguments": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" } },
|
{ "type": "block-end", "index": 1, "block": { "type": "tool-call", "id": "call_skipped", "name": "bash", "arguments": "{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}" } },
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
{"type":"step/start","seq":3,"time":1784437195076,"data":{"turn":1,"step":1}}
|
{"type":"step/start","seq":3,"time":1784437195076,"data":{"turn":1,"step":1}}
|
||||||
{"type":"request/header","seq":4,"time":1784437195076,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
{"type":"request/header","seq":4,"time":1784437195076,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||||
{"type":"assistant/chunk","seq":5,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
{"type":"assistant/chunk","seq":5,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||||
{"type":"assistant/chunk","seq":6,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_wait","name":"bash","argumentsDelta":"{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}
|
{"type":"assistant/chunk","seq":6,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_wait","name":"bash","argumentsDelta":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}
|
||||||
{"type":"assistant/chunk","seq":7,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}}
|
{"type":"assistant/chunk","seq":7,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}}}
|
||||||
{"type":"assistant/chunk","seq":8,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
{"type":"assistant/chunk","seq":8,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
|
||||||
{"type":"assistant/chunk","seq":9,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skipped","name":"bash","argumentsDelta":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}
|
{"type":"assistant/chunk","seq":9,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skipped","name":"bash","argumentsDelta":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}
|
||||||
{"type":"assistant/chunk","seq":10,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}}
|
{"type":"assistant/chunk","seq":10,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}}}
|
||||||
{"type":"assistant/chunk","seq":11,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":10}}}}
|
{"type":"assistant/chunk","seq":11,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":10}}}}
|
||||||
{"type":"assistant/chunk","seq":12,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
{"type":"assistant/chunk","seq":12,"time":1784437195077,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||||
{"type":"assistant/message","seq":13,"time":1784437195078,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"},{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":10}},"sourceEventSeqs":[5,6,7,8,9,10,11,12],"surfaceOp":"append"}
|
{"type":"assistant/message","seq":13,"time":1784437195078,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"},{"type":"tool-call","id":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":10}},"sourceEventSeqs":[5,6,7,8,9,10,11,12],"surfaceOp":"append"}
|
||||||
{"type":"tool/call","seq":14,"time":1784437195078,"data":{"turn":1,"step":1,"callId":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}
|
{"type":"tool/call","seq":14,"time":1784437195078,"data":{"turn":1,"step":1,"callId":"call_wait","name":"bash","arguments":"{\"command\":\"node -e \\\"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\\\"\",\"description\":\"Wait until cancellation\"}"}}
|
||||||
{"type":"tool/result","seq":15,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_wait","content":[{"type":"text","text":"Error: tool call aborted before dispatch"}],"isError":true,"error":{"name":"AbortError","code":"ABORTED_BEFORE_DISPATCH"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
{"type":"tool/result","seq":15,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_wait","content":[{"type":"text","text":"Error: command aborted"}],"isError":true},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||||
{"type":"tool/call","seq":16,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}
|
{"type":"tool/call","seq":16,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_skipped","name":"bash","arguments":"{\"command\":\"printf skipped > skipped.txt\",\"description\":\"Write skipped marker\"}"}}
|
||||||
{"type":"tool/result","seq":17,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_skipped","content":[{"type":"text","text":"Error: tool call aborted before dispatch"}],"isError":true,"error":{"name":"AbortError","code":"ABORTED_BEFORE_DISPATCH"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
|
{"type":"tool/result","seq":17,"time":1784437195089,"data":{"turn":1,"step":1,"callId":"call_skipped","content":[{"type":"text","text":"Error: tool call aborted before dispatch"}],"isError":true,"error":{"name":"AbortError","code":"ABORTED_BEFORE_DISPATCH"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
|
||||||
{"type":"step/end","seq":18,"time":1784437195090,"data":{"turn":1,"step":1}}
|
{"type":"step/end","seq":18,"time":1784437195090,"data":{"turn":1,"step":1}}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}}
|
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"available_commands_update","availableCommands":[{"name":"goal","description":"set or view the goal for a long-running task","input":{"hint":"[<objective>|clear|edit <objective>|pause|resume]"}},{"name":"plan","description":"Enter plan mode","input":{"hint":"[message]"}}]}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"available_commands_update","availableCommands":[{"name":"goal","description":"set or view the goal for a long-running task","input":{"hint":"[<objective>|clear|edit <objective>|pause|resume]"}},{"name":"plan","description":"Enter plan mode","input":{"hint":"[message]"}}]}}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Run two shell commands: wait","updatedAt":"{{updatedAt}}"}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Run two shell commands: wait","updatedAt":"{{updatedAt}}"}}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_wait","title":"node -e \"setInterval(() => {}, 1000)\"","kind":"execute","status":"in_progress","rawInput":"node -e \"setInterval(() => {}, 1000)\"","content":[{"type":"content","content":{"type":"text","text":"Wait until cancellation"}}]}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_wait","title":"node -e \"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\"","kind":"execute","status":"in_progress","rawInput":"node -e \"require('node:fs').writeFileSync('started.txt', 'started'); setInterval(() => {}, 1000)\"","content":[{"type":"content","content":{"type":"text","text":"Wait until cancellation"}}]}}}
|
||||||
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}}
|
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_wait","status":"failed","content":[{"type":"content","content":{"type":"text","text":"```console\nError: tool call aborted before dispatch\n```"}}]}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_wait","status":"failed","content":[{"type":"content","content":{"type":"text","text":"```console\nError: command aborted\n```"}}]}}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_skipped","title":"printf skipped > skipped.txt","kind":"execute","status":"in_progress","rawInput":"printf skipped > skipped.txt","content":[{"type":"content","content":{"type":"text","text":"Write skipped marker"}}]}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_skipped","title":"printf skipped > skipped.txt","kind":"execute","status":"in_progress","rawInput":"printf skipped > skipped.txt","content":[{"type":"content","content":{"type":"text","text":"Write skipped marker"}}]}}}
|
||||||
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_skipped","status":"failed","content":[{"type":"content","content":{"type":"text","text":"```console\nError: tool call aborted before dispatch\n```"}}]}}}
|
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_skipped","status":"failed","content":[{"type":"content","content":{"type":"text","text":"```console\nError: tool call aborted before dispatch\n```"}}]}}}
|
||||||
|
|||||||
@@ -16,18 +16,20 @@ const childScript = fileURLToPath(new URL('./fixtures/crash-child.ts', import.me
|
|||||||
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
const tsxLoader = fileURLToPath(import.meta.resolve('tsx'))
|
||||||
const sessionId = SessionId('semantic-checkpoint-crash')
|
const sessionId = SessionId('semantic-checkpoint-crash')
|
||||||
const roots: string[] = []
|
const roots: string[] = []
|
||||||
|
const CHILD_FAILPOINT_TIMEOUT_MS = 30_000
|
||||||
|
|
||||||
async function waitForFile(path: string): Promise<void> {
|
async function waitForFile(path: string): Promise<void> {
|
||||||
for (let attempt = 0; attempt < 500; attempt += 1) {
|
const deadline = Date.now() + CHILD_FAILPOINT_TIMEOUT_MS
|
||||||
|
for (;;) {
|
||||||
try {
|
try {
|
||||||
await access(path)
|
await access(path)
|
||||||
return
|
return
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||||
}
|
}
|
||||||
|
if (Date.now() >= deadline) throw new Error(`crash child did not reach failpoint ${path}`)
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
}
|
}
|
||||||
throw new Error(`crash child did not reach failpoint ${path}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function crashAt(mode: 'request' | 'tool'): Promise<{ root: string; markerText: string }> {
|
async function crashAt(mode: 'request' | 'tool'): Promise<{ root: string; markerText: string }> {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ import { launchAcpTestAgent, type AgentUnderTest, type LaunchedAcpTestAgent } fr
|
|||||||
|
|
||||||
export type { AgentUnderTest } from './launcher.ts'
|
export type { AgentUnderTest } from './launcher.ts'
|
||||||
|
|
||||||
const DEFAULT_TURN_END_TIMEOUT_MS = 10_000
|
const DEFAULT_WAIT_TIMEOUT_MS = 10_000
|
||||||
const TURN_END_POLL_INTERVAL_MS = 10
|
const WAIT_POLL_INTERVAL_MS = 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One step of a scenario's deterministic input script (`input.json`). The
|
* One step of a scenario's deterministic input script (`input.json`). The
|
||||||
@@ -46,8 +46,9 @@ const TURN_END_POLL_INTERVAL_MS = 10
|
|||||||
*
|
*
|
||||||
* `promptAndCancel` starts a prompt without awaiting completion, waits until
|
* `promptAndCancel` starts a prompt without awaiting completion, waits until
|
||||||
* the client observes the selected update (`agent_message_chunk` by default),
|
* the client observes the selected update (`agent_message_chunk` by default),
|
||||||
* then cancels and awaits completion. A named `waitForToolCallUpdate` keeps the
|
* then cancels and awaits completion. An optional `waitForFile` first observes
|
||||||
* step open for a terminal tool update that may follow the prompt response.
|
* a cwd-relative readiness marker, and a named `waitForToolCallUpdate` keeps
|
||||||
|
* the step open for a terminal tool update that may follow the prompt response.
|
||||||
* `promptAndWaitForAgentMessage` arms an exact text-chunk waiter before sending
|
* `promptAndWaitForAgentMessage` arms an exact text-chunk waiter before sending
|
||||||
* the prompt, then keeps the application live until that later update arrives.
|
* the prompt, then keeps the application live until that later update arrives.
|
||||||
* `waitForTurnEnd` holds the subprocess open until the selected session's latest
|
* `waitForTurnEnd` holds the subprocess open until the selected session's latest
|
||||||
@@ -64,6 +65,7 @@ export type InputStep =
|
|||||||
op: 'promptAndCancel'
|
op: 'promptAndCancel'
|
||||||
text: string
|
text: string
|
||||||
afterUpdate?: 'agent_message_chunk' | 'tool_call'
|
afterUpdate?: 'agent_message_chunk' | 'tool_call'
|
||||||
|
waitForFile?: { path: string; timeoutMs?: number }
|
||||||
waitForToolCallUpdate?: string
|
waitForToolCallUpdate?: string
|
||||||
}
|
}
|
||||||
| { op: 'waitForTurnEnd'; timeoutMs?: number }
|
| { op: 'waitForTurnEnd'; timeoutMs?: number }
|
||||||
@@ -445,6 +447,9 @@ async function runStep(
|
|||||||
const promptDone = client.prompt({ sessionId, prompt: [{ type: 'text', text: step.text }] })
|
const promptDone = client.prompt({ sessionId, prompt: [{ type: 'text', text: step.text }] })
|
||||||
const afterUpdate = step.afterUpdate ?? 'agent_message_chunk'
|
const afterUpdate = step.afterUpdate ?? 'agent_message_chunk'
|
||||||
await waitForUpdate(u => u.sessionUpdate === afterUpdate)
|
await waitForUpdate(u => u.sessionUpdate === afterUpdate)
|
||||||
|
if (step.waitForFile !== undefined) {
|
||||||
|
await waitForWorkspaceFile(cwd, step.waitForFile.path, step.waitForFile.timeoutMs)
|
||||||
|
}
|
||||||
// Arm this before cancellation so a fast tool drain cannot outrun the waiter.
|
// Arm this before cancellation so a fast tool drain cannot outrun the waiter.
|
||||||
const toolCallUpdateDone = step.waitForToolCallUpdate === undefined
|
const toolCallUpdateDone = step.waitForToolCallUpdate === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@@ -516,7 +521,7 @@ async function runStep(
|
|||||||
async function waitForPersistedTurnEnd(
|
async function waitForPersistedTurnEnd(
|
||||||
root: string,
|
root: string,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
timeoutMs = DEFAULT_TURN_END_TIMEOUT_MS,
|
timeoutMs = DEFAULT_WAIT_TIMEOUT_MS,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const deadline = Date.now() + timeoutMs
|
const deadline = Date.now() + timeoutMs
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -525,7 +530,23 @@ async function waitForPersistedTurnEnd(
|
|||||||
if (Date.now() >= deadline) {
|
if (Date.now() >= deadline) {
|
||||||
throw new Error(`snapshot-harness: session "${sessionId}" did not persist turn/end within ${timeoutMs}ms`)
|
throw new Error(`snapshot-harness: session "${sessionId}" did not persist turn/end within ${timeoutMs}ms`)
|
||||||
}
|
}
|
||||||
await delay(TURN_END_POLL_INTERVAL_MS)
|
await delay(WAIT_POLL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for a cwd-relative marker proving an external action reached readiness. */
|
||||||
|
async function waitForWorkspaceFile(
|
||||||
|
cwd: string,
|
||||||
|
path: string,
|
||||||
|
timeoutMs = DEFAULT_WAIT_TIMEOUT_MS,
|
||||||
|
): Promise<void> {
|
||||||
|
const target = join(cwd, path)
|
||||||
|
const deadline = Date.now() + timeoutMs
|
||||||
|
while (!existsSync(target)) {
|
||||||
|
if (Date.now() >= deadline) {
|
||||||
|
throw new Error(`snapshot-harness: workspace file "${path}" did not appear within ${timeoutMs}ms`)
|
||||||
|
}
|
||||||
|
await delay(WAIT_POLL_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -493,6 +493,37 @@ describe('runScenario', () => {
|
|||||||
expect(result.rawStdout.indexOf('thinking about it')).toBeLessThan(result.rawStdout.indexOf('cancelled'))
|
expect(result.rawStdout.indexOf('thinking about it')).toBeLessThan(result.rawStdout.indexOf('cancelled'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('promptAndCancel can wait for cwd-relative readiness before cancelling', { timeout: 20_000 }, async () => {
|
||||||
|
const { dir, fixtureFile } = await scenario({ prompt: 'hang-until-cancel' })
|
||||||
|
const workspaceDir = join(dir, 'workspace')
|
||||||
|
const { mkdir } = await import('node:fs/promises')
|
||||||
|
await mkdir(workspaceDir, { recursive: true })
|
||||||
|
await writeFile(join(workspaceDir, 'started.txt'), 'started')
|
||||||
|
const result = await runScenario(
|
||||||
|
{
|
||||||
|
steps: [...boot, {
|
||||||
|
op: 'promptAndCancel',
|
||||||
|
text: 'hang',
|
||||||
|
waitForFile: { path: 'started.txt' },
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{ agent: AGENT, mode: 'replay', fixtureFile, workspaceDir },
|
||||||
|
)
|
||||||
|
expect(result.rawStdout).toContain('"stopReason":"cancelled"')
|
||||||
|
|
||||||
|
const missing = await scenario({ prompt: 'hang-until-cancel' })
|
||||||
|
await expect(runScenario(
|
||||||
|
{
|
||||||
|
steps: [...boot, {
|
||||||
|
op: 'promptAndCancel',
|
||||||
|
text: 'hang',
|
||||||
|
waitForFile: { path: 'never.txt', timeoutMs: 20 },
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{ agent: AGENT, mode: 'replay', fixtureFile: missing.fixtureFile },
|
||||||
|
)).rejects.toThrow(/workspace file "never\.txt" did not appear within 20ms/)
|
||||||
|
})
|
||||||
|
|
||||||
it('promptAndWaitForAgentMessage keeps the app live through a matching later update', { timeout: 20_000 }, async () => {
|
it('promptAndWaitForAgentMessage keeps the app live through a matching later update', { timeout: 20_000 }, async () => {
|
||||||
const { fixtureFile } = await scenario({ prompt: 'respond' })
|
const { fixtureFile } = await scenario({ prompt: 'respond' })
|
||||||
const result = await runScenario(
|
const result = await runScenario(
|
||||||
|
|||||||
Reference in New Issue
Block a user