From 764bb310f70504206333c8aa75d1c11bae0a60c2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 05:19:25 +0100 Subject: [PATCH] ci: pin qa parity tool profile --- .../src/providers/mock-openai/server.test.ts | 79 ++++++++++++++++++- .../src/providers/mock-openai/server.ts | 10 +-- .../qa-lab/src/qa-gateway-config.test.ts | 1 + extensions/qa-lab/src/qa-gateway-config.ts | 6 ++ 4 files changed, 89 insertions(+), 7 deletions(-) diff --git a/extensions/qa-lab/src/providers/mock-openai/server.test.ts b/extensions/qa-lab/src/providers/mock-openai/server.test.ts index 55264701dc0..719bbd29742 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.test.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.test.ts @@ -1015,7 +1015,84 @@ describe("qa mock openai server", () => { { content: [ { - text: "Protocol note: delegated fanout complete. Alpha=ALPHA-OK. Beta=BETA-OK.", + text: "subagent-1: ok\nsubagent-2: ok", + }, + ], + }, + ], + }); + }); + + it("completes subagent fanout from a continuation turn without tool output", async () => { + const server = await startQaMockOpenAiServer({ + host: "127.0.0.1", + port: 0, + }); + cleanups.push(async () => { + await server.stop(); + }); + + const prompt = + "Subagent fanout synthesis check: delegate two bounded subagents sequentially, then report both results together."; + const spawn = await fetch(`${server.baseUrl}/v1/responses`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + stream: true, + tools: [SESSIONS_SPAWN_TOOL], + input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }], + }), + }); + expect(spawn.status).toBe(200); + expect(await spawn.text()).toContain('\\"label\\":\\"qa-fanout-alpha\\"'); + + const secondSpawn = await fetch(`${server.baseUrl}/v1/responses`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + stream: true, + tools: [SESSIONS_SPAWN_TOOL], + input: [ + { role: "user", content: [{ type: "input_text", text: prompt }] }, + { + type: "function_call_output", + output: + '{"status":"accepted","childSessionKey":"agent:qa:subagent:alpha","note":"ALPHA-OK"}', + }, + ], + }), + }); + expect(secondSpawn.status).toBe(200); + expect(await secondSpawn.text()).toContain('\\"label\\":\\"qa-fanout-beta\\"'); + + const phaseOnlyFinal = await fetch(`${server.baseUrl}/v1/responses`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + stream: false, + tools: [SESSIONS_SPAWN_TOOL], + input: [ + { + role: "user", + content: [ + { + type: "input_text", + text: "Continue.", + }, + ], + }, + ], + }), + }); + expect(phaseOnlyFinal.status).toBe(200); + expect(await phaseOnlyFinal.json()).toMatchObject({ + output: [ + { + content: [ + { + text: "subagent-1: ok\nsubagent-2: ok", }, ], }, diff --git a/extensions/qa-lab/src/providers/mock-openai/server.ts b/extensions/qa-lab/src/providers/mock-openai/server.ts index a8363b46a8e..d81d27792fb 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.ts @@ -745,12 +745,10 @@ function buildAssistantText( if (/fanout worker beta/i.test(prompt)) { return "BETA-OK"; } - if ( - /subagent fanout synthesis check/i.test(prompt) && - toolOutput && - scenarioState.subagentFanoutPhase >= 2 - ) { - return "Protocol note: delegated fanout complete. Alpha=ALPHA-OK. Beta=BETA-OK."; + const fanoutCompleteReply = "subagent-1: ok\nsubagent-2: ok"; + if (scenarioState.subagentFanoutPhase === 2 && prompt) { + scenarioState.subagentFanoutPhase = 3; + return fanoutCompleteReply; } if (toolOutput && (/\bdelegate\b/i.test(prompt) || /subagent handoff/i.test(prompt))) { const compact = toolOutput.replace(/\s+/g, " ").trim() || "no delegated output"; diff --git a/extensions/qa-lab/src/qa-gateway-config.test.ts b/extensions/qa-lab/src/qa-gateway-config.test.ts index 20100c36a42..8fd928fdf0c 100644 --- a/extensions/qa-lab/src/qa-gateway-config.test.ts +++ b/extensions/qa-lab/src/qa-gateway-config.test.ts @@ -63,6 +63,7 @@ describe("buildQaGatewayConfig", () => { expect(cfg.plugins?.entries?.["qa-channel"]).toEqual({ enabled: true }); expect(cfg.plugins?.entries?.openai).toBeUndefined(); expect(cfg.gateway?.reload?.deferralTimeoutMs).toBe(1_000); + expect(cfg.tools?.profile).toBe("coding"); expect(cfg.channels?.["qa-channel"]).toMatchObject({ enabled: true, baseUrl: "http://127.0.0.1:43124", diff --git a/extensions/qa-lab/src/qa-gateway-config.ts b/extensions/qa-lab/src/qa-gateway-config.ts index 360fdb136ef..f1cf799ddb8 100644 --- a/extensions/qa-lab/src/qa-gateway-config.ts +++ b/extensions/qa-lab/src/qa-gateway-config.ts @@ -180,6 +180,12 @@ export function buildQaGatewayConfig(params: { memory: { backend: "builtin", }, + tools: { + // The parity scenarios are code-agent contracts: they must always expose + // file, image, memory, and subagent tools even when the surrounding + // environment defaults to a messaging-only profile. + profile: "coding", + }, ...(gatewayModels ? { models: {