test: clear acp spawn broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 08:10:29 +01:00
parent a25072535e
commit 4643ec761b

View File

@@ -327,8 +327,70 @@ function expectAcceptedSpawn(result: SpawnResult): Extract<SpawnResult, { status
return result;
}
function expectRecordFields(
record: unknown,
expected: Record<string, unknown>,
): Record<string, unknown> {
expect(record).toBeDefined();
const actual = record as Record<string, unknown>;
for (const [key, value] of Object.entries(expected)) {
expect(actual[key]).toEqual(value);
}
return actual;
}
function gatewayRequests(): Array<{ method?: string; params?: Record<string, unknown> }> {
return hoisted.callGatewayMock.mock.calls.map(
(call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> },
);
}
function gatewayRequest(method: string): { method?: string; params?: Record<string, unknown> } {
const request = gatewayRequests().find((candidate) => candidate.method === method);
expect(request).toBeDefined();
return request as { method?: string; params?: Record<string, unknown> };
}
function expectGatewayMethodNotCalled(method: string): void {
expect(gatewayRequests().some((request) => request.method === method)).toBe(false);
}
function expectSessionPatchFields(expected: Record<string, unknown>): void {
expectRecordFields(gatewayRequest("sessions.patch").params, expected);
}
function expectInitializeSessionFields(expected: Record<string, unknown>): Record<string, unknown> {
return expectRecordFields(hoisted.initializeSessionMock.mock.calls[0]?.[0], expected);
}
function expectBindingCallFields(expected: {
conversation?: Record<string, unknown>;
metadata?: Record<string, unknown>;
placement?: string;
targetKind?: string;
}): Record<string, unknown> {
const input = expectRecordFields(hoisted.sessionBindingBindMock.mock.calls.at(-1)?.[0], {
...(expected.placement ? { placement: expected.placement } : {}),
...(expected.targetKind ? { targetKind: expected.targetKind } : {}),
});
if (expected.conversation) {
expectRecordFields(input.conversation, expected.conversation);
}
if (expected.metadata) {
expectRecordFields(input.metadata, expected.metadata);
}
return input;
}
function expectRelayCallFields(expected: Record<string, unknown>, callIndex = 0): void {
expectRecordFields(
hoisted.startAcpSpawnParentStreamRelayMock.mock.calls[callIndex]?.[0],
expected,
);
}
function expectAgentGatewayCall(overrides: AgentCallParams): void {
const agentCall = findAgentGatewayCall();
const agentCall = gatewayRequest("agent");
expect(agentCall?.params?.deliver).toBe(overrides.deliver);
expect(agentCall?.params?.channel).toBe(overrides.channel);
expect(agentCall?.params?.to).toBe(overrides.to);
@@ -689,37 +751,28 @@ describe("spawnAcpDirect", () => {
expect(accepted.runId).toBe("run-1");
expect(accepted.mode).toBe("session");
expect(accepted.inlineDelivery).toBe(true);
const patchCall = hoisted.callGatewayMock.mock.calls
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
.find((request) => request.method === "sessions.patch");
expect(patchCall?.params).toMatchObject({
expectSessionPatchFields({
key: accepted.childSessionKey,
spawnedBy: "agent:main:main",
});
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
targetKind: "session",
placement: "child",
}),
);
expectBindingCallFields({
targetKind: "session",
placement: "child",
});
expectResolvedIntroTextInBindMetadata();
const agentCall = hoisted.callGatewayMock.mock.calls
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
.find((request) => request.method === "agent");
const agentCall = gatewayRequest("agent");
expect(agentCall?.params?.sessionKey).toMatch(/^agent:codex:acp:/);
expect(agentCall?.params?.to).toBe("channel:child-thread");
expect(agentCall?.params?.threadId).toBe("child-thread");
expect(agentCall?.params?.deliver).toBe(true);
expect(agentCall?.params?.lane).toBe("subagent");
expect(agentCall?.params?.acpTurnSource).toBe("manual_spawn");
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
agent: "codex",
mode: "persistent",
}),
);
const initInput = expectInitializeSessionFields({
agent: "codex",
mode: "persistent",
});
expect(initInput.sessionKey).toMatch(/^agent:codex:acp:/);
const transcriptCalls = hoisted.resolveSessionTranscriptFileMock.mock.calls.map(
(call: unknown[]) => call[0] as { threadId?: string },
);
@@ -765,11 +818,7 @@ describe("spawnAcpDirect", () => {
);
expectAcceptedSpawn(result);
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
resumeSessionId,
}),
);
expectInitializeSessionFields({ resumeSessionId });
});
it("rejects ACP resume IDs not recorded for the requester session", async () => {
@@ -807,7 +856,7 @@ describe("spawnAcpDirect", () => {
},
);
expect(result).toMatchObject({
expectRecordFields(result, {
status: "forbidden",
errorCode: "resume_forbidden",
});
@@ -829,16 +878,14 @@ describe("spawnAcpDirect", () => {
);
expectAcceptedSpawn(result);
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
agent: "codex",
runtimeOptions: {
model: "openai-codex/gpt-5.4",
thinking: "high",
},
}),
);
const initInput = expectInitializeSessionFields({
agent: "codex",
runtimeOptions: {
model: "openai-codex/gpt-5.4",
thinking: "high",
},
});
expect(initInput.sessionKey).toMatch(/^agent:codex:acp:/);
});
it("applies ACP spawn run timeout to runtime options and dispatch", async () => {
@@ -854,15 +901,13 @@ describe("spawnAcpDirect", () => {
);
expectAcceptedSpawn(result);
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
agent: "codex",
runtimeOptions: {
timeoutSeconds: 45,
},
}),
);
const initInput = expectInitializeSessionFields({
agent: "codex",
runtimeOptions: {
timeoutSeconds: 45,
},
});
expect(initInput.sessionKey).toMatch(/^agent:codex:acp:/);
const agentCall = findAgentGatewayCall();
expect(agentCall?.params?.lane).toBe("subagent");
expect(agentCall?.params?.timeout).toBe(45);
@@ -897,15 +942,13 @@ describe("spawnAcpDirect", () => {
},
);
expect(result).toMatchObject({
expectRecordFields(result, {
status: "error",
errorCode: "runtime_agent_mismatch",
});
expect(result).toHaveProperty("error", expect.stringContaining("OpenClaw config agent"));
expect(hoisted.initializeSessionMock).not.toHaveBeenCalled();
expect(hoisted.callGatewayMock).not.toHaveBeenCalledWith(
expect.objectContaining({ method: "agent" }),
);
expectGatewayMethodNotCalled("agent");
});
it("maps OpenClaw ACP runtime agent aliases to their configured harness id", async () => {
@@ -943,12 +986,8 @@ describe("spawnAcpDirect", () => {
);
expectAcceptedSpawn(result);
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
agent: "codex",
sessionKey: expect.stringMatching(/^agent:codex:acp:/),
}),
);
const initInput = expectInitializeSessionFields({ agent: "codex" });
expect(initInput.sessionKey).toMatch(/^agent:codex:acp:/);
});
it("inherits subagent envelope fields onto ACP children", async () => {
@@ -971,10 +1010,7 @@ describe("spawnAcpDirect", () => {
});
const accepted = expectAcceptedSpawn(result);
const patchCall = hoisted.callGatewayMock.mock.calls
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
.find((request) => request.method === "sessions.patch");
expect(patchCall?.params).toMatchObject({
expectSessionPatchFields({
key: accepted.childSessionKey,
spawnedBy: "agent:main:subagent:parent",
spawnDepth: 2,
@@ -1261,16 +1297,14 @@ describe("spawnAcpDirect", () => {
},
);
expect(result.status, JSON.stringify(result)).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "child",
conversation: expect.objectContaining({
channel: "matrix",
accountId: "default",
conversationId: "!room:example",
}),
}),
);
expectBindingCallFields({
placement: "child",
conversation: {
channel: "matrix",
accountId: "default",
conversationId: "!room:example",
},
});
expectAgentGatewayCall({
deliver: true,
channel: "matrix",
@@ -1321,16 +1355,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status, JSON.stringify(result)).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "child",
conversation: expect.objectContaining({
channel: "matrix",
accountId: "default",
conversationId: "!Room:Example.org",
}),
}),
);
expectBindingCallFields({
placement: "child",
conversation: {
channel: "matrix",
accountId: "default",
conversationId: "!Room:Example.org",
},
});
expectAgentGatewayCall({
deliver: true,
channel: "matrix",
@@ -1382,17 +1414,15 @@ describe("spawnAcpDirect", () => {
);
expect(result.status, JSON.stringify(result)).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "child",
conversation: expect.objectContaining({
channel: "matrix",
accountId: "default",
conversationId: "$thread-root",
parentConversationId: "!Room:Example.org",
}),
}),
);
expectBindingCallFields({
placement: "child",
conversation: {
channel: "matrix",
accountId: "default",
conversationId: "$thread-root",
parentConversationId: "!Room:Example.org",
},
});
expectAgentGatewayCall({
deliver: true,
channel: "matrix",
@@ -1418,13 +1448,11 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:claude-code:acp:/),
agent: "claude-code",
cwd: fixture.targetWorkspace,
}),
);
const initInput = expectInitializeSessionFields({
agent: "claude-code",
cwd: fixture.targetWorkspace,
});
expect(initInput.sessionKey).toMatch(/^agent:claude-code:acp:/);
} finally {
await fs.rm(fixture.workspaceRoot, { recursive: true, force: true });
}
@@ -1450,13 +1478,11 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.initializeSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: expect.stringMatching(/^agent:claude-code:acp:/),
agent: "claude-code",
cwd: undefined,
}),
);
const initInput = expectInitializeSessionFields({
agent: "claude-code",
cwd: undefined,
});
expect(initInput.sessionKey).toMatch(/^agent:claude-code:acp:/);
} finally {
await fs.rm(fixture.workspaceRoot, { recursive: true, force: true });
}
@@ -1534,16 +1560,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status, JSON.stringify(result)).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "line",
accountId: "default",
conversationId: "U1234567890abcdef1234567890abcdef",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "line",
accountId: "default",
conversationId: "U1234567890abcdef1234567890abcdef",
},
});
expectAgentGatewayCall({
deliver: true,
channel: "line",
@@ -1629,16 +1653,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "custom",
accountId: "work",
conversationId: "123456",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "custom",
accountId: "work",
conversationId: "123456",
},
});
expectAgentGatewayCall({
deliver: true,
channel: "custom",
@@ -1740,17 +1762,15 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "child",
conversation: expect.objectContaining({
channel: "matrix",
accountId: "bot-alpha",
conversationId: boundRoom,
}),
}),
);
expect(findAgentGatewayCall()?.params).toMatchObject({
expectBindingCallFields({
placement: "child",
conversation: {
channel: "matrix",
accountId: "bot-alpha",
conversationId: boundRoom,
},
});
expectRecordFields(gatewayRequest("agent").params, {
deliver: true,
channel: "matrix",
accountId: "bot-alpha",
@@ -1820,16 +1840,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "line",
accountId: "default",
conversationId: expectedConversationId,
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "line",
accountId: "default",
conversationId: expectedConversationId,
},
});
},
);
@@ -1873,16 +1891,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "line",
accountId: "default",
conversationId: "R1234567890abcdef1234567890abcdef",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "line",
accountId: "default",
conversationId: "R1234567890abcdef1234567890abcdef",
},
});
});
it.each([
@@ -1919,13 +1935,11 @@ describe("spawnAcpDirect", () => {
expect(accepted.streamLogPath).toBeUndefined();
expect(hoisted.startAcpSpawnParentStreamRelayMock).not.toHaveBeenCalled();
if (expectTranscriptPersistence) {
expect(hoisted.resolveSessionTranscriptFileMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "sess-123",
storePath: "/tmp/codex-sessions.json",
agentId: "codex",
}),
);
expectRecordFields(hoisted.resolveSessionTranscriptFileMock.mock.calls[0]?.[0], {
sessionId: "sess-123",
storePath: "/tmp/codex-sessions.json",
agentId: "codex",
});
}
expectAgentGatewayCall(expectedAgentCall);
});
@@ -1974,13 +1988,10 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
metadata: expect.objectContaining({
introText: expect.stringContaining("cwd: /home/bob/clawd"),
}),
}),
);
const bindInput = expectBindingCallFields({});
const metadata = expectRecordFields(bindInput.metadata, {});
expect(typeof metadata.introText).toBe("string");
expect(metadata.introText).toContain("cwd: /home/bob/clawd");
});
it("rejects disallowed ACP agents", async () => {
@@ -2003,7 +2014,7 @@ describe("spawnAcpDirect", () => {
},
);
expect(result).toMatchObject({
expectRecordFields(result, {
status: "forbidden",
});
});
@@ -2132,22 +2143,22 @@ describe("spawnAcpDirect", () => {
expect(typeof relayCallOrder).toBe("number");
expect(typeof agentCallOrder).toBe("number");
expect(relayCallOrder < agentCallOrder).toBe(true);
expect(hoisted.startAcpSpawnParentStreamRelayMock).toHaveBeenCalledWith(
expect.objectContaining({
parentSessionKey: "agent:main:main",
agentId: "codex",
logPath: "/tmp/sess-main.acp-stream.jsonl",
emitStartNotice: false,
}),
);
expectRelayCallFields({
parentSessionKey: "agent:main:main",
agentId: "codex",
logPath: "/tmp/sess-main.acp-stream.jsonl",
emitStartNotice: false,
});
const relayRuns = hoisted.startAcpSpawnParentStreamRelayMock.mock.calls.map(
(call: unknown[]) => (call[0] as { runId?: string }).runId,
);
expect(relayRuns).toContain(agentCall?.params?.idempotencyKey);
expect(relayRuns).toContain(accepted.runId);
expect(hoisted.resolveAcpSpawnStreamLogPathMock).toHaveBeenCalledWith({
childSessionKey: expect.stringMatching(/^agent:codex:acp:/),
});
const streamPathInput = expectRecordFields(
hoisted.resolveAcpSpawnStreamLogPathMock.mock.calls[0]?.[0],
{},
);
expect(streamPathInput.childSessionKey).toMatch(/^agent:codex:acp:/);
expect(firstHandle.dispose).toHaveBeenCalledTimes(1);
expect(firstHandle.notifyStarted).not.toHaveBeenCalled();
expect(secondHandle.notifyStarted).toHaveBeenCalledTimes(1);
@@ -2220,19 +2231,17 @@ describe("spawnAcpDirect", () => {
expect(agentCall?.params?.channel).toBeUndefined();
expect(agentCall?.params?.to).toBeUndefined();
expect(agentCall?.params?.threadId).toBeUndefined();
expect(hoisted.startAcpSpawnParentStreamRelayMock).toHaveBeenCalledWith(
expect.objectContaining({
parentSessionKey: "agent:main:subagent:parent",
agentId: "codex",
logPath: "/tmp/sess-main.acp-stream.jsonl",
deliveryContext: {
channel: "discord",
to: "channel:parent-channel",
accountId: "default",
},
emitStartNotice: false,
}),
);
expectRelayCallFields({
parentSessionKey: "agent:main:subagent:parent",
agentId: "codex",
logPath: "/tmp/sess-main.acp-stream.jsonl",
deliveryContext: {
channel: "discord",
to: "channel:parent-channel",
accountId: "default",
},
emitStartNotice: false,
});
expect(firstHandle.dispose).toHaveBeenCalledTimes(1);
expect(secondHandle.notifyStarted).toHaveBeenCalledTimes(1);
});
@@ -2570,17 +2579,15 @@ describe("spawnAcpDirect", () => {
const accepted = expectAcceptedSpawn(result);
expect(accepted.mode).toBe("session");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "telegram",
accountId: "default",
conversationId: "2",
parentConversationId: "-1003342490704",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "2",
parentConversationId: "-1003342490704",
},
});
const agentCall = hoisted.callGatewayMock.mock.calls
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
.find((request) => request.method === "agent");
@@ -2608,16 +2615,14 @@ describe("spawnAcpDirect", () => {
const accepted = expectAcceptedSpawn(result);
expect(accepted.mode).toBe("session");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "telegram",
accountId: "default",
conversationId: "6098642967",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "6098642967",
},
});
const bindCall = hoisted.sessionBindingBindMock.mock.calls.at(-1)?.[0] as
| { conversation?: { parentConversationId?: string } }
| undefined;
@@ -2643,16 +2648,14 @@ describe("spawnAcpDirect", () => {
);
expect(result.status).toBe("accepted");
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: "current",
conversation: expect.objectContaining({
channel: "telegram",
accountId: "default",
conversationId: "-1003342490704:topic:2",
}),
}),
);
expectBindingCallFields({
placement: "current",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "-1003342490704:topic:2",
},
});
});
it("disposes pre-registered parent relay when initial ACP dispatch fails", async () => {