Skip to content

Commit 5800e0d

Browse files
committed
test(e2e): interrupted child resumes and reports back to parent
1 parent e0700a2 commit 5800e0d

8 files changed

Lines changed: 263 additions & 56 deletions

File tree

apps/cli/src/ui/components/autocomplete/triggers/HistoryTrigger.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface HistoryResult extends AutocompleteItem {
2121
/** Mode the task was run in */
2222
mode?: string
2323
/** Task status */
24-
status?: "active" | "completed" | "delegated"
24+
status?: "active" | "completed" | "delegated" | "interrupted"
2525
}
2626

2727
/**
@@ -133,8 +133,22 @@ export function createHistoryTrigger(config: HistoryTriggerConfig): Autocomplete
133133

134134
renderItem: (item: HistoryResult, isSelected: boolean) => {
135135
// Status indicator
136-
const statusIcon = item.status === "completed" ? "✓" : item.status === "active" ? "●" : "○"
137-
const statusColor = item.status === "completed" ? "green" : item.status === "active" ? "yellow" : "gray"
136+
const statusIcon =
137+
item.status === "completed"
138+
? "✓"
139+
: item.status === "active"
140+
? "●"
141+
: item.status === "interrupted"
142+
? "⏸"
143+
: "○"
144+
const statusColor =
145+
item.status === "completed"
146+
? "green"
147+
: item.status === "active"
148+
? "yellow"
149+
: item.status === "interrupted"
150+
? "cyan"
151+
: "gray"
138152

139153
// Mode indicator (if available)
140154
const modeText = item.mode ? ` [${item.mode}]` : ""
@@ -178,7 +192,7 @@ export function toHistoryResult(item: {
178192
totalCost?: number
179193
workspace?: string
180194
mode?: string
181-
status?: "active" | "completed" | "delegated"
195+
status?: "active" | "completed" | "delegated" | "interrupted"
182196
}): HistoryResult {
183197
return {
184198
key: item.id, // Use task ID as the unique key

apps/cli/src/ui/components/autocomplete/triggers/__tests__/HistoryTrigger.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@ describe("HistoryTrigger", () => {
188188
expect(output).toContain("○")
189189
})
190190

191+
it("should render interrupted status with correct indicator", () => {
192+
const trigger = createHistoryTrigger({ getHistory: () => mockHistoryItems })
193+
194+
const interruptedItem: HistoryResult = {
195+
key: "task-interrupted",
196+
id: "task-interrupted",
197+
task: "Interrupted subtask waiting to resume",
198+
ts: Date.now() - 1000 * 60 * 5,
199+
mode: "ask",
200+
status: "interrupted",
201+
}
202+
const { lastFrame } = render(trigger.renderItem(interruptedItem, false) as React.ReactElement)
203+
204+
const output = lastFrame()
205+
// Should contain the interrupted status indicator (⏸)
206+
expect(output).toContain("⏸")
207+
})
208+
191209
it("should render selected items with different styling", () => {
192210
const trigger = createHistoryTrigger({ getHistory: () => mockHistoryItems })
193211

apps/cli/src/ui/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export interface TaskHistoryItem {
109109
totalCost?: number
110110
workspace?: string
111111
mode?: string
112-
status?: "active" | "completed" | "delegated"
112+
status?: "active" | "completed" | "delegated" | "interrupted"
113113
tokensIn?: number
114114
tokensOut?: number
115115
}

apps/vscode-e2e/src/fixtures/subtasks.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { toolResultContains } from "./tool-result"
55

66
const SUBTASK_PARENT_MARKER = "SUBTASK_PARENT_CANCELLATION_SMOKE"
77
const SUBTASK_CHILD_MARKER = "SUBTASK_CHILD_CALCULATOR_SMOKE"
8+
const SUBTASK_INTERRUPT_PARENT_MARKER = "SUBTASK_PARENT_INTERRUPT_RESUME"
9+
const SUBTASK_INTERRUPT_CHILD_MARKER = "SUBTASK_CHILD_INTERRUPT_RESUME"
810
const SUBTASK_FAST_PARENT_MARKER = "SUBTASK_PARENT_IMMEDIATE_COMPLETION"
911
const SUBTASK_FAST_CHILD_MARKER = "SUBTASK_CHILD_IMMEDIATE_COMPLETION"
1012
const SUBTASK_XPROFILE_PARENT_MARKER = "SUBTASK_PARENT_CROSS_PROFILE"
@@ -17,6 +19,11 @@ export const SUBTASK_CHILD_FOLLOWUP_ANSWER = "9"
1719
const SUBTASK_FAST_CHILD_PROMPT = `${SUBTASK_FAST_CHILD_MARKER}: Complete immediately with the exact result "Fast child completed".`
1820
export const SUBTASK_FAST_PARENT_PROMPT = `${SUBTASK_FAST_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_FAST_CHILD_PROMPT}" Do not answer directly.`
1921

22+
const SUBTASK_INTERRUPT_CHILD_PROMPT = `${SUBTASK_INTERRUPT_CHILD_MARKER}: Ask the user exactly this follow-up question: What is the square root of 81? After the user answers, complete with only the answer.`
23+
export const SUBTASK_INTERRUPT_PARENT_PROMPT = `${SUBTASK_INTERRUPT_PARENT_MARKER}: Use the new_task tool exactly once. Create an ask-mode subtask with this exact message: "${SUBTASK_INTERRUPT_CHILD_PROMPT}" Do not answer directly. When the subtask returns, complete with the exact result "Interrupted parent resumed".`
24+
export const SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER = "9"
25+
export const SUBTASK_INTERRUPT_PARENT_RESULT = "Interrupted parent resumed"
26+
2027
const SUBTASK_XPROFILE_SAME_CHILD_PROMPT = `${SUBTASK_XPROFILE_SAME_CHILD_MARKER}: Complete immediately with the exact result "Same-profile child completed".`
2128
const SUBTASK_XPROFILE_DIFFERENT_CHILD_PROMPT = `${SUBTASK_XPROFILE_DIFFERENT_CHILD_MARKER}: Complete immediately with the exact result "Different-profile child completed".`
2229
export const SUBTASK_XPROFILE_PARENT_PROMPT = `${SUBTASK_XPROFILE_PARENT_MARKER}: First use new_task to create a code-mode subtask with this exact message: "${SUBTASK_XPROFILE_SAME_CHILD_PROMPT}" After it returns, create an ask-mode subtask with the next instructions you receive.`
@@ -252,4 +259,89 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
252259
],
253260
},
254261
})
262+
263+
// Interrupted-child-resumes-and-reports-back scenario (#560)
264+
mock.addFixture({
265+
match: {
266+
userMessage: new RegExp(SUBTASK_INTERRUPT_PARENT_MARKER),
267+
sequenceIndex: 0,
268+
},
269+
response: {
270+
toolCalls: [
271+
{
272+
name: "new_task",
273+
arguments: JSON.stringify({
274+
mode: "ask",
275+
message: SUBTASK_INTERRUPT_CHILD_PROMPT,
276+
}),
277+
id: "call_interrupt_parent_new_task_001",
278+
},
279+
],
280+
},
281+
})
282+
283+
// The parent prompt embeds SUBTASK_INTERRUPT_CHILD_MARKER verbatim, so parent-resume turns
284+
// also match a bare substring check. Exclude the parent marker so they fall through.
285+
mock.addFixture({
286+
match: {
287+
predicate: (req: ChatCompletionRequest) =>
288+
requestContains(req, [SUBTASK_INTERRUPT_CHILD_MARKER]) &&
289+
!requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER]) &&
290+
!requestContains(req, ["call_interrupt_child_followup_001"]) &&
291+
!requestContains(req, [SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER]),
292+
},
293+
response: {
294+
toolCalls: [
295+
{
296+
name: "ask_followup_question",
297+
arguments: JSON.stringify({
298+
question: "What is the square root of 81?",
299+
follow_up: [{ text: SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER }],
300+
}),
301+
id: "call_interrupt_child_followup_001",
302+
},
303+
],
304+
},
305+
})
306+
307+
mock.addFixture({
308+
match: {
309+
predicate: (req: ChatCompletionRequest) =>
310+
// Preferred: structured tool-result message carries the followup answer.
311+
toolResultContains(req, "call_interrupt_child_followup_001", [
312+
SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER,
313+
]) ||
314+
// Fallback 1: answer present alongside the tool-call ID.
315+
requestContains(req, ["call_interrupt_child_followup_001", SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER]) ||
316+
// Fallback 2: answer arrives as a bare user message after task resume.
317+
requestContains(req, [
318+
SUBTASK_INTERRUPT_CHILD_MARKER,
319+
`<user_message>\\n${SUBTASK_INTERRUPT_CHILD_FOLLOWUP_ANSWER}\\n</user_message>`,
320+
]),
321+
},
322+
response: {
323+
toolCalls: [
324+
{
325+
name: "attempt_completion",
326+
arguments: JSON.stringify({ result: "9" }),
327+
id: "call_interrupt_child_completion_002",
328+
},
329+
],
330+
},
331+
})
332+
333+
mock.addFixture({
334+
match: {
335+
toolCallId: "call_interrupt_parent_new_task_001",
336+
},
337+
response: {
338+
toolCalls: [
339+
{
340+
name: "attempt_completion",
341+
arguments: JSON.stringify({ result: "Interrupted parent resumed" }),
342+
id: "call_interrupt_parent_completion_003",
343+
},
344+
],
345+
},
346+
})
255347
}

0 commit comments

Comments
 (0)