Skip to content

Commit dcae4b7

Browse files
committed
test(e2e-mock): fix subtasks fixture collision between child and parent-resume fixtures (#1001)
The fast-child and SUBTASK_CHILD fixtures matched a bare marker that the parent prompt embeds verbatim, so parent-resume turns could be served the child fixture (mirrors #561, which fixed the same class for cross-profile fixtures only). Parent-resume fixtures also guarded on the literal new_task tool-call id, which validateAndFixToolResultIds can rewrite on resume. - Add parent-marker exclusion (with aimock's last-user-message scoping) to the fast-child and SUBTASK_CHILD fixtures - Guard parent-resume fixtures on the injected subtask result content instead of the fragile tool-call id
1 parent 6fec173 commit dcae4b7

1 file changed

Lines changed: 33 additions & 10 deletions

File tree

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ const requestContains = (req: ChatCompletionRequest, expected: string[]) => {
5454
return expected.every((text) => rawRequest.includes(text))
5555
}
5656

57+
// aimock's `userMessage` matcher only inspects the LAST user message. Fixtures that need
58+
// whole-request exclusions must replicate that scoping inside a predicate so they keep the
59+
// same matching semantics as the bare-regex fixtures they replace.
60+
const lastUserMessageContains = (req: ChatCompletionRequest, text: string) => {
61+
const userMessages = req.messages?.filter((message) => message.role === "user") ?? []
62+
const last = userMessages.at(-1)
63+
if (!last) return false
64+
const content = typeof last.content === "string" ? last.content : JSON.stringify(last.content ?? "")
65+
return content.includes(text)
66+
}
67+
68+
// reopenParentFromDelegation injects the child result into the resumed parent's history as
69+
// `Subtask <childId> completed.\n\nResult:\n<summary>`. Matching on this injected prefix (in
70+
// its JSON-serialized form) keeps parent-resume fixtures robust when
71+
// validateAndFixToolResultIds rewrites tool-use ids on resume — matching on the new_task
72+
// tool-call id directly proved flaky (the id can be rewritten, the fixture then misses, and
73+
// a looser child fixture wins and serves the child's response to the parent).
74+
const SUBTASK_RESULT_INJECTION = "completed.\\n\\nResult:"
75+
5776
const completionAfterAnswer = (followupId: string, completionId: string) => ({
5877
match: {
5978
predicate: (req: ChatCompletionRequest) =>
@@ -100,9 +119,14 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
100119
},
101120
})
102121

122+
// The parent prompt embeds SUBTASK_FAST_CHILD_MARKER verbatim, so parent-resume turns
123+
// can also match a bare substring check (same collision class as #561). Exclude the
124+
// parent marker so those turns fall through to the parent-resume fixture below.
103125
mock.addFixture({
104126
match: {
105-
userMessage: new RegExp(SUBTASK_FAST_CHILD_MARKER),
127+
predicate: (req: ChatCompletionRequest) =>
128+
lastUserMessageContains(req, SUBTASK_FAST_CHILD_MARKER) &&
129+
!requestContains(req, [SUBTASK_FAST_PARENT_MARKER]),
106130
},
107131
response: {
108132
toolCalls: [
@@ -118,7 +142,7 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
118142
mock.addFixture({
119143
match: {
120144
predicate: (req: ChatCompletionRequest) =>
121-
requestContains(req, [SUBTASK_FAST_PARENT_MARKER, "call_subtasks_fast_parent_new_task_001"]),
145+
requestContains(req, [SUBTASK_FAST_PARENT_MARKER, "Fast child completed"]),
122146
},
123147
response: {
124148
toolCalls: [
@@ -149,9 +173,12 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
149173
},
150174
})
151175

176+
// Same collision guard as the fast-child fixture above: SUBTASK_PARENT_PROMPT embeds
177+
// SUBTASK_CHILD_MARKER verbatim, so parent-resume turns must not match this fixture.
152178
mock.addFixture({
153179
match: {
154-
userMessage: new RegExp(SUBTASK_CHILD_MARKER),
180+
predicate: (req: ChatCompletionRequest) =>
181+
lastUserMessageContains(req, SUBTASK_CHILD_MARKER) && !requestContains(req, [SUBTASK_PARENT_MARKER]),
155182
},
156183
response: {
157184
toolCalls: [
@@ -172,7 +199,7 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
172199
mock.addFixture({
173200
match: {
174201
predicate: (req: ChatCompletionRequest) =>
175-
requestContains(req, [SUBTASK_PARENT_MARKER, "call_subtasks_parent_new_task_001"]),
202+
requestContains(req, [SUBTASK_PARENT_MARKER, SUBTASK_RESULT_INJECTION]),
176203
},
177204
response: {
178205
toolCalls: [
@@ -241,11 +268,7 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
241268
mock.addFixture({
242269
match: {
243270
predicate: (req: ChatCompletionRequest) =>
244-
requestContains(req, [
245-
SUBTASK_API_HANG_PARENT_MARKER,
246-
"call_api_hang_parent_new_task_001",
247-
SUBTASK_API_HANG_CHILD_RESULT,
248-
]),
271+
requestContains(req, [SUBTASK_API_HANG_PARENT_MARKER, SUBTASK_API_HANG_CHILD_RESULT]),
249272
},
250273
response: {
251274
toolCalls: [
@@ -433,7 +456,7 @@ export function addSubtaskFixtures(mock: InstanceType<typeof LLMock>) {
433456
mock.addFixture({
434457
match: {
435458
predicate: (req: ChatCompletionRequest) =>
436-
requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER, "call_interrupt_parent_new_task_001"]),
459+
requestContains(req, [SUBTASK_INTERRUPT_PARENT_MARKER, SUBTASK_RESULT_INJECTION]),
437460
},
438461
response: {
439462
toolCalls: [

0 commit comments

Comments
 (0)