refactor: simplify gateway and e2e test helpers

This commit is contained in:
Peter Steinberger
2026-04-29 12:14:15 +01:00
parent 9d1c5a77c2
commit 1d494af03a
9 changed files with 223 additions and 363 deletions

View File

@@ -222,13 +222,10 @@ run_wrapper_flow() {
export USER="testuser"
mkdir -p "$HOME/.local/bin"
local wrapper="$HOME/.local/bin/openclaw-wrapper"
cat >"$wrapper" <<WRAPPER
#!/usr/bin/env bash
set -euo pipefail
printf "%s\n" "\$@" >> "$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"

View File

@@ -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 <wrapper-path> <npm-bin> [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 },
);

View File

@@ -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 <<EOF
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw"
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 <<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: $purpose
'@ | 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
}
parallels_child_job_running() {
local target="$1"
local owner="${2:-}"

View File

@@ -794,22 +794,8 @@ verify_gateway_status() {
}
prepare_agent_workspace() {
guest_exec /bin/sh -lc 'set -eu
workspace="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
mkdir -p "$workspace/.openclaw"
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() {

View File

@@ -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 <<EOF
guest_current_user_sh "$(cat <<EOF
export PATH=$(shell_quote "$GUEST_EXEC_PATH")
workspace="\${OPENCLAW_WORKSPACE_DIR:-\$HOME/.openclaw/workspace}"
mkdir -p "\$workspace/.openclaw"
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 \

View File

@@ -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

View File

@@ -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
}

View File

@@ -159,6 +159,7 @@ const makeContext = (): GatewayRequestContext =>
type AgentHandlerArgs = Parameters<typeof agentHandlers.agent>[0];
type AgentParams = AgentHandlerArgs["params"];
type AgentCommandCall = Record<string, unknown>;
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<T> {
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<string, unknown>;
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<string, unknown>)?.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");
});

View File

@@ -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<SessionEntry> = {}) {
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"),
},
});