diff --git a/scripts/e2e/lib/doctor-install-switch/scenario.sh b/scripts/e2e/lib/doctor-install-switch/scenario.sh index 4808391a86a..f752513d3af 100644 --- a/scripts/e2e/lib/doctor-install-switch/scenario.sh +++ b/scripts/e2e/lib/doctor-install-switch/scenario.sh @@ -222,13 +222,10 @@ run_wrapper_flow() { export USER="testuser" mkdir -p "$HOME/.local/bin" local wrapper="$HOME/.local/bin/openclaw-wrapper" - cat >"$wrapper" <> "$HOME/openclaw-wrapper-argv.log" -exec "$npm_bin" "\$@" -WRAPPER - chmod +x "$wrapper" + node scripts/e2e/lib/doctor-install-switch/write-wrapper.mjs \ + "$wrapper" \ + "$npm_bin" \ + "$HOME/openclaw-wrapper-argv.log" local unit_path="$HOME/.config/systemd/user/openclaw-gateway.service" diff --git a/scripts/e2e/lib/doctor-install-switch/write-wrapper.mjs b/scripts/e2e/lib/doctor-install-switch/write-wrapper.mjs new file mode 100644 index 00000000000..8e92f955708 --- /dev/null +++ b/scripts/e2e/lib/doctor-install-switch/write-wrapper.mjs @@ -0,0 +1,24 @@ +#!/usr/bin/env node +import fs from "node:fs"; + +const [, , wrapperPath, npmBin, logPath = `${process.env.HOME}/openclaw-wrapper-argv.log`] = + process.argv; + +if (!wrapperPath || !npmBin || !logPath || logPath.startsWith("undefined/")) { + console.error("usage: write-wrapper.mjs [log-path]"); + process.exit(1); +} + +function shellSingleQuote(value) { + return `'${value.replaceAll("'", "'\\''")}'`; +} + +fs.writeFileSync( + wrapperPath, + `#!/usr/bin/env bash +set -euo pipefail +printf "%s\\n" "$@" >> ${shellSingleQuote(logPath)} +exec ${shellSingleQuote(npmBin)} "$@" +`, + { mode: 0o755 }, +); diff --git a/scripts/e2e/lib/parallels-package-common.sh b/scripts/e2e/lib/parallels-package-common.sh index 58d223a3a70..6afdad8347f 100644 --- a/scripts/e2e/lib/parallels-package-common.sh +++ b/scripts/e2e/lib/parallels-package-common.sh @@ -62,6 +62,52 @@ parallels_log_progress_extract() { node scripts/e2e/lib/parallels-package/log-progress-extract.mjs "$log_path" } +parallels_bash_seed_workspace_snippet() { + local purpose="$1" + cat < "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' +# Identity + +- Name: OpenClaw +- Purpose: $purpose +IDENTITY_EOF +cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF' +{ + "version": 1, + "setupCompletedAt": "2026-01-01T00:00:00.000Z" +} +STATE_EOF +rm -f "\$workspace/BOOTSTRAP.md" +EOF +} + +parallels_powershell_seed_workspace_snippet() { + local purpose="$1" + cat < "$workspace/IDENTITY.md" <<'"'"'IDENTITY_EOF'"'"' -# Identity - -- Name: OpenClaw -- Purpose: Parallels Linux smoke test assistant. -IDENTITY_EOF -cat > "$workspace/.openclaw/workspace-state.json" <<'"'"'STATE_EOF'"'"' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -STATE_EOF -rm -f "$workspace/BOOTSTRAP.md"' + guest_exec /bin/sh -lc "set -eu +$(parallels_bash_seed_workspace_snippet "Parallels Linux smoke test assistant.")" } verify_local_turn() { diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 3f3e111757e..b0455ba805b 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -1500,23 +1500,9 @@ show_gateway_status_compat() { verify_turn() { guest_current_user_exec "$GUEST_NODE_BIN" "$GUEST_OPENCLAW_ENTRY" models set "$MODEL_ID" guest_current_user_exec "$GUEST_NODE_BIN" "$GUEST_OPENCLAW_ENTRY" config set agents.defaults.skipBootstrap true --strict-json - guest_current_user_sh "$(cat < "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' -# Identity - -- Name: OpenClaw -- Purpose: Parallels macOS smoke test assistant. -IDENTITY_EOF -cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -STATE_EOF -rm -f "\$workspace/BOOTSTRAP.md" +$(parallels_bash_seed_workspace_snippet "Parallels macOS smoke test assistant.") exec /usr/bin/env $(shell_quote "$API_KEY_ENV=$API_KEY_VALUE") \ $(shell_quote "$GUEST_NODE_BIN") $(shell_quote "$GUEST_OPENCLAW_ENTRY") agent \ --agent main \ diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh index d37e8376f65..c17d648f0fe 100755 --- a/scripts/e2e/parallels-npm-update-smoke.sh +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -1097,21 +1097,7 @@ if [ "\$gateway_ready" != "1" ]; then echo "gateway did not become ready after transport recovery" >&2 exit 1 fi -workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}" -mkdir -p "\$workspace/.openclaw" -cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' -# Identity - -- Name: OpenClaw -- Purpose: Parallels npm update smoke test assistant. -IDENTITY_EOF -cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -STATE_EOF -rm -f "\$workspace/BOOTSTRAP.md" +$(parallels_bash_seed_workspace_snippet "Parallels npm update smoke test assistant.") /opt/homebrew/bin/openclaw models set "$MODEL_ID" /opt/homebrew/bin/openclaw config set agents.defaults.skipBootstrap true --strict-json /opt/homebrew/bin/openclaw agent --agent main --session-id "parallels-npm-update-macos-transport-recovery-$expected_needle" --message "Reply with exact ASCII text OK only." --json @@ -1734,21 +1720,7 @@ fi if [ "\$gateway_ready" != "1" ]; then /opt/homebrew/bin/openclaw gateway status --deep --require-rpc fi -workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}" -mkdir -p "\$workspace/.openclaw" -cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' -# Identity - -- Name: OpenClaw -- Purpose: Parallels npm update smoke test assistant. -IDENTITY_EOF -cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -STATE_EOF -rm -f "\$workspace/BOOTSTRAP.md" +$(parallels_bash_seed_workspace_snippet "Parallels npm update smoke test assistant.") /opt/homebrew/bin/openclaw agent --agent main --session-id parallels-npm-update-macos-$expected_needle --message "Reply with exact ASCII text OK only." --json EOF macos_desktop_user_exec /bin/bash /tmp/openclaw-main-update.sh @@ -1838,21 +1810,7 @@ fi openclaw update status --json openclaw models set "$MODEL_ID" openclaw config set agents.defaults.skipBootstrap true --strict-json -workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}" -mkdir -p "\$workspace/.openclaw" -cat > "\$workspace/IDENTITY.md" <<'IDENTITY_EOF' -# Identity - -- Name: OpenClaw -- Purpose: Parallels npm update smoke test assistant. -IDENTITY_EOF -cat > "\$workspace/.openclaw/workspace-state.json" <<'STATE_EOF' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -STATE_EOF -rm -f "\$workspace/BOOTSTRAP.md" +$(parallels_bash_seed_workspace_snippet "Parallels npm update smoke test assistant.") openclaw agent --local --agent main --session-id parallels-npm-update-linux-$expected_needle --message "Reply with exact ASCII text OK only." --json EOF prlctl exec "$LINUX_VM" /usr/bin/env "$API_KEY_ENV=$API_KEY_VALUE" /bin/bash /tmp/openclaw-main-update.sh diff --git a/scripts/e2e/parallels-windows-smoke.sh b/scripts/e2e/parallels-windows-smoke.sh index 5c8ddd3450b..3a6e41625e3 100644 --- a/scripts/e2e/parallels-windows-smoke.sh +++ b/scripts/e2e/parallels-windows-smoke.sh @@ -2589,28 +2589,7 @@ show_gateway_status_compat() { verify_turn() { guest_run_openclaw "" "" models set "$MODEL_ID" guest_run_openclaw "" "" config set agents.defaults.skipBootstrap true --strict-json - guest_powershell "$(cat <<'EOF' -$workspace = $env:OPENCLAW_WORKSPACE_DIR -if (-not $workspace) { - $workspace = Join-Path $env:USERPROFILE '.openclaw\workspace' -} -$stateDir = Join-Path $workspace '.openclaw' -New-Item -ItemType Directory -Path $stateDir -Force | Out-Null -@' -# Identity - -- Name: OpenClaw -- Purpose: Parallels Windows smoke test assistant. -'@ | Set-Content -Path (Join-Path $workspace 'IDENTITY.md') -Encoding UTF8 -@' -{ - "version": 1, - "setupCompletedAt": "2026-01-01T00:00:00.000Z" -} -'@ | Set-Content -Path (Join-Path $stateDir 'workspace-state.json') -Encoding UTF8 -Remove-Item (Join-Path $workspace 'BOOTSTRAP.md') -Force -ErrorAction SilentlyContinue -EOF - )" + guest_powershell "$(parallels_powershell_seed_workspace_snippet "Parallels Windows smoke test assistant.")" stop_gateway_processes_for_local_agent_turn guest_run_agent_turn_process } diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 54b979d4c2e..9184173fec2 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -159,6 +159,7 @@ const makeContext = (): GatewayRequestContext => type AgentHandlerArgs = Parameters[0]; type AgentParams = AgentHandlerArgs["params"]; +type AgentCommandCall = Record; type AgentIdentityGetHandlerArgs = Parameters<(typeof agentHandlers)["agent.identity.get"]>[0]; type AgentIdentityGetParams = AgentIdentityGetHandlerArgs["params"]; @@ -254,9 +255,8 @@ function resetTimeConfig() { } async function expectResetCall(expectedMessage: string) { - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + const call = await waitForAgentCommandCall(); expect(mocks.performGatewaySessionReset).toHaveBeenCalledTimes(1); - const call = readLastAgentCommandCall(); expect(call?.message).toBe(expectedMessage); return call; } @@ -308,15 +308,19 @@ async function runMainAgentAndCaptureEntry(idempotencyKey: string) { return capturedEntry; } -function readLastAgentCommandCall(): - | { - message?: string; - sessionId?: string; - } - | undefined { - return mocks.agentCommand.mock.calls.at(-1)?.[0] as - | { message?: string; sessionId?: string } - | undefined; +function readLastAgentCommandCall(): AgentCommandCall | undefined { + return mocks.agentCommand.mock.calls.at(-1)?.[0] as AgentCommandCall | undefined; +} + +async function waitForAgentCommandCall< + T extends AgentCommandCall = AgentCommandCall, +>(): Promise { + await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + const call = readLastAgentCommandCall(); + if (!call) { + throw new Error("expected agentCommand call"); + } + return call as T; } function mockSessionResetSuccess(params: { @@ -551,8 +555,7 @@ describe("gateway agent handler", () => { }, ); - const lastCall = mocks.agentCommand.mock.calls.at(-1); - expect(lastCall?.[0]).toEqual( + await expect(waitForAgentCommandCall()).resolves.toEqual( expect.objectContaining({ provider: "anthropic", model: "claude-haiku-4-5", @@ -574,9 +577,7 @@ describe("gateway agent handler", () => { { reqId: "test-acp-turn-source" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const lastCall = mocks.agentCommand.mock.calls.at(-1); - expect(lastCall?.[0]).toEqual( + await expect(waitForAgentCommandCall()).resolves.toEqual( expect.objectContaining({ acpTurnSource: "manual_spawn", }), @@ -643,8 +644,7 @@ describe("gateway agent handler", () => { }, ); - const lastCall = mocks.agentCommand.mock.calls.at(-1); - expect(lastCall?.[0]).toEqual( + await expect(waitForAgentCommandCall()).resolves.toEqual( expect.objectContaining({ provider: "anthropic", model: "claude-haiku-4-5", @@ -859,10 +859,7 @@ describe("gateway agent handler", () => { { reqId: "ts-1" }, ); - // Wait for the async agentCommand call - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - - const callArgs = mocks.agentCommand.mock.calls[0][0]; + const callArgs = await waitForAgentCommandCall<{ message?: string }>(); expect(callArgs.message).toBe("[Wed 2026-01-28 20:30 EST] Is it the weekend?"); resetTimeConfig(); @@ -887,9 +884,7 @@ describe("gateway agent handler", () => { { reqId: "inter-session-marker" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - - const callArgs = mocks.agentCommand.mock.calls[0][0] as { message?: string }; + const callArgs = await waitForAgentCommandCall<{ message?: string }>(); expect(callArgs.message).toMatch(/^\[Inter-session message\]/); expect(callArgs.message).toContain("isUser=false"); expect(callArgs.message).toContain("forwarded reply"); @@ -924,13 +919,11 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - - const callArgs = mocks.agentCommand.mock.calls[0][0] as { + const callArgs = await waitForAgentCommandCall<{ message?: string; modelRun?: boolean; promptMode?: string; - }; + }>(); expect(callArgs).toEqual( expect.objectContaining({ message: "Reply exactly: pong", @@ -976,11 +969,8 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as - | { senderIsOwner?: boolean } - | undefined; - expect(callArgs?.senderIsOwner).toBe(senderIsOwner); + const callArgs = await waitForAgentCommandCall<{ senderIsOwner?: boolean }>(); + expect(callArgs.senderIsOwner).toBe(senderIsOwner); }); it("respects explicit bestEffortDeliver=false for main session runs", async () => { @@ -1001,8 +991,7 @@ describe("gateway agent handler", () => { { reqId: "strict-1" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as Record; + const callArgs = await waitForAgentCommandCall(); expect(callArgs.bestEffortDeliver).toBe(false); }); @@ -1036,7 +1025,7 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + await waitForAgentCommandCall(); const accepted = respond.mock.calls.find( (call: unknown[]) => call[0] === true && (call[1] as Record)?.status === "accepted", @@ -1158,7 +1147,7 @@ describe("gateway agent handler", () => { { reqId: "music-generation-event-1", respond }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + await waitForAgentCommandCall(); expect(respond).not.toHaveBeenCalledWith( false, undefined, @@ -1205,7 +1194,7 @@ describe("gateway agent handler", () => { { reqId: "music-generation-event-inter-session" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + await waitForAgentCommandCall(); expect(findTaskByRunId("music-generation-event-inter-session")).toBeUndefined(); }); @@ -1234,8 +1223,7 @@ describe("gateway agent handler", () => { }, { reqId: "workspace-forwarded-1" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const spawnedCall = mocks.agentCommand.mock.calls.at(-1)?.[0] as { workspaceDir?: string }; + const spawnedCall = await waitForAgentCommandCall<{ workspaceDir?: string }>(); expect(spawnedCall.workspaceDir).toBe("/tmp/inherited"); }); @@ -1277,12 +1265,11 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const callArgs = await waitForAgentCommandCall<{ channel?: string; messageChannel?: string; runContext?: { messageChannel?: string }; - }; + }>(); expect(callArgs.channel).toBe("telegram"); expect(callArgs.messageChannel).toBe("webchat"); expect(callArgs.runContext?.messageChannel).toBe("webchat"); @@ -1467,15 +1454,14 @@ describe("gateway agent handler", () => { { reqId: "session-id-agent-resume" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const call = await waitForAgentCommandCall<{ agentId?: string; sessionId?: string; sessionKey?: string; - }; - expect(call?.agentId).toBe("main"); - expect(call?.sessionId).toBe("resume-whatsapp-session"); - expect(call?.sessionKey).toBeUndefined(); + }>(); + expect(call.agentId).toBe("main"); + expect(call.sessionId).toBe("resume-whatsapp-session"); + expect(call.sessionKey).toBeUndefined(); }); it("treats whitespace sessionId as absent before resolving the agent session key", async () => { @@ -1496,15 +1482,14 @@ describe("gateway agent handler", () => { { reqId: "blank-session-id-agent-resume" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const call = await waitForAgentCommandCall<{ agentId?: string; sessionId?: string; sessionKey?: string; - }; - expect(call?.agentId).toBe("main"); - expect(call?.sessionId).toBe("existing-session-id"); - expect(call?.sessionKey).toBe("agent:main:main"); + }>(); + expect(call.agentId).toBe("main"); + expect(call.sessionId).toBe("existing-session-id"); + expect(call.sessionKey).toBe("agent:main:main"); }); it("rolls stale gateway agent sessions even when updatedAt was recently touched", async () => { @@ -1554,13 +1539,12 @@ describe("gateway agent handler", () => { { reqId: "daily-rollover-agent-session" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const call = await waitForAgentCommandCall<{ sessionId?: string; sessionKey?: string; - }; - expect(call?.sessionKey).toBe("agent:main:main"); - expect(call?.sessionId).not.toBe("stale-session-id"); + }>(); + expect(call.sessionKey).toBe("agent:main:main"); + expect(call.sessionId).not.toBe("stale-session-id"); expect(capturedEntry?.sessionStartedAt).toBe(now); expect(capturedEntry?.lastInteractionAt).toBe(now); } finally { @@ -1616,13 +1600,12 @@ describe("gateway agent handler", () => { { reqId: "daily-rollover-agent-session-id" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const call = await waitForAgentCommandCall<{ sessionId?: string; sessionKey?: string; - }; - expect(call?.sessionKey).toBe("agent:main:main"); - expect(call?.sessionId).not.toBe("stale-session-id"); + }>(); + expect(call.sessionKey).toBe("agent:main:main"); + expect(call.sessionId).not.toBe("stale-session-id"); expect(capturedEntry?.sessionStartedAt).toBe(now); expect(capturedEntry?.lastInteractionAt).toBe(now); } finally { @@ -1662,13 +1645,12 @@ describe("gateway agent handler", () => { { reqId: "global-session-agent-id" }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = mocks.agentCommand.mock.calls.at(-1)?.[0] as { + const call = await waitForAgentCommandCall<{ agentId?: string; sessionKey?: string; - }; - expect(call?.agentId).toBeUndefined(); - expect(call?.sessionKey).toBe("global"); + }>(); + expect(call.agentId).toBeUndefined(); + expect(call.sessionKey).toBe("global"); }); it("dispatches async gateway agent task creation through the detached task runtime seam", async () => { @@ -1766,8 +1748,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:voice"); }); @@ -1810,8 +1791,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:main"); }); @@ -1854,8 +1834,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:voice"); expect(mocks.resolveVoiceWakeRouteByTrigger).toHaveBeenCalledWith({ trigger: undefined, @@ -1904,8 +1883,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:voice"); expect(mocks.resolveVoiceWakeRouteByTrigger).toHaveBeenCalledWith({ trigger: "robot wake", @@ -1954,8 +1932,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:research"); expect(mocks.loadVoiceWakeRoutingConfig).not.toHaveBeenCalled(); expect(mocks.resolveVoiceWakeRouteByTrigger).not.toHaveBeenCalled(); @@ -2002,8 +1979,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:ops:main"); expect(mocks.loadVoiceWakeRoutingConfig).not.toHaveBeenCalled(); expect(mocks.resolveVoiceWakeRouteByTrigger).not.toHaveBeenCalled(); @@ -2051,8 +2027,7 @@ describe("gateway agent handler", () => { }, ); - await vi.waitFor(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const callArgs = mocks.agentCommand.mock.calls.at(-1)?.[0] as { sessionKey?: string }; + const callArgs = await waitForAgentCommandCall<{ sessionKey?: string }>(); expect(callArgs.sessionKey).toBe("agent:main:main"); expect(mocks.loadVoiceWakeRoutingConfig).not.toHaveBeenCalled(); expect(mocks.resolveVoiceWakeRouteByTrigger).not.toHaveBeenCalled(); @@ -2129,9 +2104,8 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); expect(mocks.performGatewaySessionReset).toHaveBeenCalledTimes(1); - const call = readLastAgentCommandCall(); + const call = await waitForAgentCommandCall(); // Message is now dynamically built with current date — check key substrings expect(call?.message).toContain("Execute your Session Startup sequence now"); expect(call?.message).toContain("Current time:"); @@ -2168,8 +2142,7 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = readLastAgentCommandCall(); + const call = await waitForAgentCommandCall(); expect(call?.message).toContain("[Startup context loaded by runtime]"); expect(call?.message).toContain("[Untrusted daily memory: memory/2026-01-28.md]"); expect(call?.message).toContain("today gateway note"); @@ -2204,8 +2177,7 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = readLastAgentCommandCall(); + const call = await waitForAgentCommandCall(); expect(call?.message).toContain("while bootstrap is still pending for this workspace"); expect(call?.message).toContain("Please read BOOTSTRAP.md from the workspace now"); expect(call?.message).not.toContain("Today memory context"); @@ -2257,8 +2229,7 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = readLastAgentCommandCall(); + const call = await waitForAgentCommandCall(); expect(call?.message).toContain("while bootstrap is still pending for this workspace"); expect(call?.message).toContain( "cannot safely complete the full BOOTSTRAP.md workflow here", @@ -2311,8 +2282,7 @@ describe("gateway agent handler", () => { }, ); - await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); - const call = readLastAgentCommandCall(); + const call = await waitForAgentCommandCall(); expect(call?.message).toContain("Execute your Session Startup sequence now"); expect(call?.message).not.toContain("while bootstrap is still pending for this workspace"); }); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 552130daa9f..3155ad04ef9 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import type { AssistantMessage, UserMessage } from "@mariozechner/pi-ai"; import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; import { WebSocket } from "ws"; +import type { SessionEntry } from "../config/sessions.js"; import { isSessionPatchEvent, type InternalHookEvent } from "../hooks/internal-hooks.js"; import { enqueueSystemEvent, @@ -273,6 +274,14 @@ async function writeSingleLineSession(dir: string, sessionId: string, content: s ); } +function sessionStoreEntry(sessionId: string, overrides: Partial = {}) { + return { + sessionId, + updatedAt: Date.now(), + ...overrides, + }; +} + async function createCheckpointFixture(dir: string) { const { SessionManager } = await getSessionManagerModule(); const session = SessionManager.create(dir, dir); @@ -341,7 +350,7 @@ async function seedActiveMainSession() { await writeSingleLineSession(dir, "sess-main", "hello"); await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, + main: sessionStoreEntry("sess-main"), }, }); return { dir, storePath }; @@ -486,10 +495,7 @@ describe("gateway server sessions", () => { piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; await writeSessionStore({ entries: { - main: { - sessionId: "sess-parent", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-parent"), }, }); const created = await directSessionReq<{ @@ -733,12 +739,8 @@ describe("gateway server sessions", () => { ); await writeSessionStore({ entries: { - main: { - sessionId: "sess-parent", - updatedAt: Date.now(), - }, - "dashboard:child": { - sessionId: "sess-child", + main: sessionStoreEntry("sess-parent"), + "dashboard:child": sessionStoreEntry("sess-child", { updatedAt: Date.now() - 1_000, modelProvider: "anthropic", model: "claude-sonnet-4-6", @@ -749,7 +751,7 @@ describe("gateway server sessions", () => { outputTokens: 0, cacheRead: 0, cacheWrite: 0, - }, + }), }, }); @@ -792,12 +794,10 @@ describe("gateway server sessions", () => { }; await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { modelProvider: "test-provider", model: "reasoner", - }, + }), }, }); @@ -846,10 +846,7 @@ describe("gateway server sessions", () => { await createSessionStoreDir(); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-main"), }, }); @@ -921,15 +918,13 @@ describe("gateway server sessions", () => { ); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { modelProvider: "openai-codex", model: "gpt-5.3-codex-spark", contextTokens: 123_456, totalTokens: 0, totalTokensFresh: false, - }, + }), }, }); @@ -980,9 +975,7 @@ describe("gateway server sessions", () => { await createSessionStoreDir(); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { verboseLevel: "on", responseUsage: "full", fastMode: true, @@ -990,7 +983,7 @@ describe("gateway server sessions", () => { lastTo: "-100123", lastAccountId: "acct-1", lastThreadId: 42, - }, + }), }, }); @@ -1042,11 +1035,9 @@ describe("gateway server sessions", () => { await createSessionStoreDir(); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { sendPolicy: "deny", - }, + }), }, }); @@ -1092,16 +1083,14 @@ describe("gateway server sessions", () => { await createSessionStoreDir(); await writeSessionStore({ entries: { - "subagent:child": { - sessionId: "sess-child", - updatedAt: Date.now(), + "subagent:child": sessionStoreEntry("sess-child", { spawnedBy: "agent:main:main", spawnedWorkspaceDir: "/tmp/subagent-workspace", forkedFromParent: true, spawnDepth: 2, subagentRole: "orchestrator", subagentControlScope: "children", - }, + }), }, }); @@ -1561,10 +1550,8 @@ describe("gateway server sessions", () => { const { SessionManager } = await getSessionManagerModule(); await writeSessionStore({ entries: { - main: { - sessionId: fixture.sessionId, + main: sessionStoreEntry(fixture.sessionId, { sessionFile: fixture.sessionFile, - updatedAt: Date.now(), compactionCheckpoints: [ { checkpointId: "checkpoint-1", @@ -1589,7 +1576,7 @@ describe("gateway server sessions", () => { }, }, ], - }, + }), }, }); @@ -1713,12 +1700,10 @@ describe("gateway server sessions", () => { ); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { thinkingLevel: "medium", reasoningLevel: "stream", - }, + }), }, }); @@ -1767,10 +1752,7 @@ describe("gateway server sessions", () => { await fs.writeFile( storePath, JSON.stringify({ - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + "agent:main:main": sessionStoreEntry("sess-main"), }), "utf-8", ); @@ -1847,10 +1829,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId, - updatedAt: Date.now(), - }, + main: sessionStoreEntry(sessionId), }, }); @@ -1879,13 +1858,11 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-stale-model", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-stale-model", { modelProvider: "qwencode", model: "qwen3.5-plus-2026-02-15", contextTokens: 123456, - }, + }), }, }); @@ -1921,14 +1898,12 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-explicit-model-override", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-explicit-model-override", { providerOverride: "anthropic", modelOverride: "claude-opus-4-1", modelProvider: "openai", model: "gpt-test-a", - }, + }), }, }); @@ -1978,16 +1953,14 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-fallback-model-override", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-fallback-model-override", { providerOverride: "anthropic", modelOverride: "claude-opus-4-1", modelOverrideSource: "auto", fallbackNoticeSelectedModel: "openai/gpt-test-a", fallbackNoticeActiveModel: "anthropic/claude-opus-4-1", fallbackNoticeReason: "rate limit", - }, + }), }, }); @@ -2033,16 +2006,14 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-fallback-stale-default", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-fallback-stale-default", { providerOverride: "anthropic", modelOverride: "claude-opus-4-1", modelOverrideSource: "auto", fallbackNoticeSelectedModel: "openai/gpt-test-a", fallbackNoticeActiveModel: "anthropic/claude-opus-4-1", fallbackNoticeReason: "rate limit", - }, + }), }, }); @@ -2086,10 +2057,8 @@ describe("gateway server sessions", () => { ); await writeSessionStore({ entries: { - "subagent:child": { - sessionId: "sess-owned-child", + "subagent:child": sessionStoreEntry("sess-owned-child", { sessionFile: customSessionFile, - updatedAt: Date.now(), chatType: "group", channel: "discord", groupId: "group-1", @@ -2141,7 +2110,7 @@ describe("gateway server sessions", () => { threadId: "thread-1", }, label: "owned child", - }, + }), }, }); @@ -2591,11 +2560,8 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, - "discord:group:dev": { - sessionId: "sess-active", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-main"), + "discord:group:dev": sessionStoreEntry("sess-active"), }, }); @@ -2652,16 +2618,12 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - "agent:main:dreaming-narrative-owned": { - sessionId: "sess-owned", - updatedAt: Date.now(), + "agent:main:dreaming-narrative-owned": sessionStoreEntry("sess-owned", { pluginOwnerId: "memory-core", - }, - "agent:main:dreaming-narrative-foreign": { - sessionId: "sess-foreign", - updatedAt: Date.now(), + }), + "agent:main:dreaming-narrative-foreign": sessionStoreEntry("sess-foreign", { pluginOwnerId: "other-plugin", - }, + }), }, }); @@ -2706,10 +2668,8 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, - "discord:group:dev": { - sessionId: "sess-acp", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main"), + "discord:group:dev": sessionStoreEntry("sess-acp", { acp: { backend: "acpx", agent: "codex", @@ -2718,7 +2678,7 @@ describe("gateway server sessions", () => { state: "idle", lastActivityAt: Date.now(), }, - }, + }), }, }); const deleted = await directSessionReq<{ ok: true; deleted: boolean }>("sessions.delete", { @@ -2757,12 +2717,10 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, - "discord:group:delete": { - sessionId: "sess-delete", + main: sessionStoreEntry("sess-main"), + "discord:group:delete": sessionStoreEntry("sess-delete", { sessionFile: transcriptPath, - updatedAt: Date.now(), - }, + }), }, }); @@ -2799,7 +2757,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-main", "hello"); await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, + main: sessionStoreEntry("sess-main"), }, }); @@ -2818,10 +2776,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-subagent", "hello"); await writeSessionStore({ entries: { - "agent:main:subagent:worker": { - sessionId: "sess-subagent", - updatedAt: Date.now(), - }, + "agent:main:subagent:worker": sessionStoreEntry("sess-subagent"), }, }); @@ -2854,10 +2809,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-subagent", "hello"); await writeSessionStore({ entries: { - "agent:main:subagent:worker": { - sessionId: "sess-subagent", - updatedAt: Date.now(), - }, + "agent:main:subagent:worker": sessionStoreEntry("sess-subagent"), }, }); @@ -2880,10 +2832,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-subagent", "hello"); await writeSessionStore({ entries: { - "agent:main:subagent:worker": { - sessionId: "sess-subagent", - updatedAt: Date.now(), - }, + "agent:main:subagent:worker": sessionStoreEntry("sess-subagent"), }, }); subagentLifecycleHookState.hasSubagentEndedHook = false; @@ -2970,9 +2919,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-main", { acp: { backend: "acpx", agent: "codex", @@ -2993,7 +2940,7 @@ describe("gateway server sessions", () => { state: "idle", lastActivityAt: Date.now(), }, - }, + }), }, }); const reset = await directSessionReq<{ @@ -3096,7 +3043,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-main", "hello"); await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, + main: sessionStoreEntry("sess-main"), }, }); @@ -3118,10 +3065,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-subagent", "hello"); await writeSessionStore({ entries: { - "agent:main:subagent:worker": { - sessionId: "sess-subagent", - updatedAt: Date.now(), - }, + "agent:main:subagent:worker": sessionStoreEntry("sess-subagent"), }, }); @@ -3159,10 +3103,7 @@ describe("gateway server sessions", () => { await writeSingleLineSession(dir, "sess-main", "hello"); await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-main"), }, }); subagentLifecycleHookState.hasSubagentEndedHook = false; @@ -3185,7 +3126,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { sessionId: "sess-main", updatedAt: Date.now() }, + main: sessionStoreEntry("sess-main"), }, }); @@ -3444,11 +3385,9 @@ describe("gateway server sessions", () => { gatewayStorePath, JSON.stringify( { - "agent:main:main": { - sessionId: "sess-new", + "agent:main:main": sessionStoreEntry("sess-new", { sessionFile: newTranscriptPath, - updatedAt: Date.now(), - }, + }), }, null, 2, @@ -3490,10 +3429,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - "discord:group:dev": { - sessionId: "sess-active", - updatedAt: Date.now(), - }, + "discord:group:dev": sessionStoreEntry("sess-active"), }, }); @@ -3534,10 +3470,8 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: fixture.sessionId, + main: sessionStoreEntry(fixture.sessionId, { sessionFile: fixture.sessionFile, - updatedAt: Date.now(), compactionCheckpoints: [ { checkpointId: "checkpoint-1", @@ -3562,11 +3496,8 @@ describe("gateway server sessions", () => { }, }, ], - }, - "discord:group:dev": { - sessionId: "sess-group", - updatedAt: Date.now(), - }, + }), + "discord:group:dev": sessionStoreEntry("sess-group"), }, }); @@ -3622,11 +3553,9 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-hook-test", - updatedAt: Date.now(), + main: sessionStoreEntry("sess-hook-test", { label: "original-label", - }, + }), }, }); @@ -3668,10 +3597,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-webchat-test", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-webchat-test"), }, }); @@ -3710,10 +3636,7 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-success-test", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-success-test"), }, }); @@ -3775,10 +3698,7 @@ describe("gateway server sessions", () => { await createSessionStoreDir(); await writeSessionStore({ entries: { - main: { - sessionId: "sess-cfg-isolation-test", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-cfg-isolation-test"), }, }); @@ -3827,14 +3747,8 @@ describe("gateway server sessions", () => { await writeSessionStore({ entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - "discord:group:dev": { - sessionId: "sess-group", - updatedAt: Date.now(), - }, + main: sessionStoreEntry("sess-main"), + "discord:group:dev": sessionStoreEntry("sess-group"), }, });