diff --git a/extensions/openai/openai-provider.live.test.ts b/extensions/openai/openai-provider.live.test.ts index a3f5ae47171..2779a3c09ad 100644 --- a/extensions/openai/openai-provider.live.test.ts +++ b/extensions/openai/openai-provider.live.test.ts @@ -155,8 +155,11 @@ describeLive("buildOpenAIProvider live", () => { const response = await client.responses.create({ model: normalized?.id ?? liveCase.modelId, - input: "Reply with exactly OK.", - max_output_tokens: 16, + instructions: "Return exactly OK and no other text.", + input: "Return exactly OK.", + max_output_tokens: 64, + reasoning: { effort: "none" }, + text: { verbosity: "low" }, }); expect(response.output_text.trim()).toMatch(/^OK[.!]?$/); diff --git a/extensions/openai/openai.live.test.ts b/extensions/openai/openai.live.test.ts index 812785fcec6..b96b06168a9 100644 --- a/extensions/openai/openai.live.test.ts +++ b/extensions/openai/openai.live.test.ts @@ -223,8 +223,11 @@ describeLive("openai plugin live", () => { }); const response = await client.responses.create({ model: normalized?.id ?? LIVE_MODEL_ID, - input: "Reply with exactly OK.", - max_output_tokens: 16, + instructions: "Return exactly OK and no other text.", + input: "Return exactly OK.", + max_output_tokens: 64, + reasoning: { effort: "none" }, + text: { verbosity: "low" }, }); expect(response.output_text.trim()).toMatch(/^OK[.!]?$/); @@ -274,7 +277,7 @@ describeLive("openai plugin live", () => { const ttsConfig = createLiveTtsConfig(); const synthesized = await speechProvider.synthesize({ - text: "OpenClaw integration test OK.", + text: "Open claw. Open claw. Integration test OK.", cfg, providerConfig: ttsConfig.providerConfigs.openai ?? {}, target: "audio-file", 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 dfd10dd1460..8d0dace3ba9 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.test.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.test.ts @@ -1377,6 +1377,80 @@ describe("qa mock openai server", () => { }); }); + it("does not let fanout completion state hijack child worker replies", 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 childReply = await fetch(`${server.baseUrl}/v1/responses`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + stream: false, + input: [ + { + role: "user", + content: [ + { + type: "input_text", + text: "Fanout worker alpha: inspect the QA workspace and finish with exactly ALPHA-OK.", + }, + ], + }, + ], + }), + }); + expect(childReply.status).toBe(200); + expect(await childReply.json()).toMatchObject({ + output: [ + { + content: [ + { + text: "ALPHA-OK", + }, + ], + }, + ], + }); + }); + it("keeps subagent fanout state isolated per mock server instance", async () => { const serverA = await startQaMockOpenAiServer({ host: "127.0.0.1", diff --git a/extensions/qa-lab/src/providers/mock-openai/server.ts b/extensions/qa-lab/src/providers/mock-openai/server.ts index a6c3aa9d385..7992a6ece48 100644 --- a/extensions/qa-lab/src/providers/mock-openai/server.ts +++ b/extensions/qa-lab/src/providers/mock-openai/server.ts @@ -795,7 +795,9 @@ function buildAssistantText( return "FORKED-CONTEXT-ALPHA"; } const fanoutCompleteReply = "subagent-1: ok\nsubagent-2: ok"; - if (scenarioState.subagentFanoutPhase === 2 && prompt) { + const isFanoutCompletionTurn = + /subagent fanout synthesis check/i.test(allInputText) || /^continue\.?$/i.test(prompt.trim()); + if (scenarioState.subagentFanoutPhase === 2 && prompt && isFanoutCompletionTurn) { scenarioState.subagentFanoutPhase = 3; return fanoutCompleteReply; } @@ -1191,6 +1193,12 @@ async function buildResponsesPayload( if (isHeartbeatPrompt(prompt)) { return buildAssistantEvents("HEARTBEAT_OK"); } + if (/fanout worker alpha/i.test(prompt)) { + return buildAssistantEvents("ALPHA-OK"); + } + if (/fanout worker beta/i.test(prompt)) { + return buildAssistantEvents("BETA-OK"); + } if (QA_REASONING_ONLY_RECOVERY_PROMPT_RE.test(allInputText)) { if (!toolOutput) { return buildToolCallEventsWithArgs("read", { path: "QA_KICKOFF_TASK.md" }); diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 066e54fb705..6c060d1f35e 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -585,6 +585,14 @@ async function runFreshLane(params) { logPath: join(params.logsDir, "fresh-onboard.log"), }); + logLanePhase(lane, "models-set"); + await runModelsSet({ + lane, + env, + providerConfig: params.providerConfig, + logPath: join(params.logsDir, "fresh-models-set.log"), + }); + logLanePhase(lane, "start-gateway"); const gateway = await startGateway({ lane, @@ -606,14 +614,6 @@ async function runFreshLane(params) { logPath: join(params.logsDir, "fresh-dashboard.log"), }); - logLanePhase(lane, "models-set"); - await runModelsSet({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "fresh-models-set.log"), - }); - logLanePhase(lane, "agent-turn"); const agent = await runAgentTurn({ lane, @@ -720,6 +720,14 @@ async function runUpgradeLane(params) { logPath: join(params.logsDir, "upgrade-onboard.log"), }); + logLanePhase(lane, "models-set"); + await runModelsSet({ + lane, + env, + providerConfig: params.providerConfig, + logPath: join(params.logsDir, "upgrade-models-set.log"), + }); + logLanePhase(lane, "start-gateway"); const gateway = await startGateway({ lane, @@ -741,14 +749,6 @@ async function runUpgradeLane(params) { logPath: join(params.logsDir, "upgrade-dashboard.log"), }); - logLanePhase(lane, "models-set"); - await runModelsSet({ - lane, - env, - providerConfig: params.providerConfig, - logPath: join(params.logsDir, "upgrade-models-set.log"), - }); - logLanePhase(lane, "agent-turn"); const agent = await runAgentTurn({ lane, @@ -837,6 +837,15 @@ async function runInstallerFreshSuite(params) { }); } + logLanePhase(lane, "models-set"); + await runInstalledModelsSet({ + cliPath: freshShell.cliPath, + env, + providerConfig: params.providerConfig, + cwd: lane.homeDir, + logPath: join(params.logsDir, "installer-fresh-models-set.log"), + }); + if (!useManagedGatewayAfterInstall) { // Keep the Windows installer lane validating Scheduled Task registration during // onboarding and lifecycle commands, but use a manual gateway for the runtime @@ -884,15 +893,6 @@ async function runInstallerFreshSuite(params) { logPath: join(params.logsDir, "installer-fresh-dashboard.log"), }); - logLanePhase(lane, "models-set"); - await runInstalledModelsSet({ - cliPath: freshShell.cliPath, - env, - providerConfig: params.providerConfig, - cwd: lane.homeDir, - logPath: join(params.logsDir, "installer-fresh-models-set.log"), - }); - logLanePhase(lane, "agent-turn"); const agent = await runInstalledAgentTurn({ cliPath: freshShell.cliPath, @@ -1023,6 +1023,15 @@ async function runDevUpdateSuite(params) { logPath: join(params.logsDir, "dev-update-onboard.log"), }); + logLanePhase(lane, "models-set"); + await runInstalledModelsSet({ + cliPath: verifiedShell.cliPath, + env, + providerConfig: params.providerConfig, + cwd: lane.homeDir, + logPath: join(params.logsDir, "dev-update-models-set.log"), + }); + if (!useManagedGatewayAfterDevUpdate) { logLanePhase(lane, "gateway-start"); const gateway = await startManualGatewayFromInstalledCli({ @@ -1056,15 +1065,6 @@ async function runDevUpdateSuite(params) { logPath: join(params.logsDir, "dev-update-dashboard.log"), }); - logLanePhase(lane, "models-set"); - await runInstalledModelsSet({ - cliPath: verifiedShell.cliPath, - env, - providerConfig: params.providerConfig, - cwd: lane.homeDir, - logPath: join(params.logsDir, "dev-update-models-set.log"), - }); - logLanePhase(lane, "agent-turn"); const agent = await runInstalledAgentTurn({ cliPath: verifiedShell.cliPath,