test: clear gateway agent event broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 13:09:33 +01:00
parent 1ed50b0ced
commit c7af05776f
+178 -143
View File
@@ -155,6 +155,44 @@ describe("agent event handler", () => {
return call;
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
expect(typeof value).toBe("object");
expect(value).not.toBeNull();
if (typeof value !== "object" || value === null) {
throw new Error(`${label} was not an object`);
}
return value as Record<string, unknown>;
}
function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
for (const [key, value] of Object.entries(fields)) {
expect(record[key]).toEqual(value);
}
}
function expectPayloadFields(value: unknown, fields: Record<string, unknown>) {
expectRecordFields(requireRecord(value, "event payload"), fields);
}
function expectPayloadDataFields(value: unknown, fields: Record<string, unknown>) {
const payload = requireRecord(value, "event payload");
expectRecordFields(requireRecord(payload.data, "event payload data"), fields);
}
function requireMockPayload(
mock: ReturnType<typeof vi.fn>,
index: number,
payloadIndex: number,
label: string,
) {
const call = mock.mock.calls[index];
expect(call).toBeDefined();
if (!call) {
throw new Error(`missing ${label} call ${index + 1}`);
}
return requireRecord(call[payloadIndex], label);
}
const FALLBACK_LIFECYCLE_DATA = {
phase: "fallback",
selectedProvider: "fireworks",
@@ -816,15 +854,12 @@ describe("agent event handler", () => {
const nodeToolCalls = nodeSendToSession.mock.calls.filter(([, event]) => event === "agent");
expect(nodeToolCalls).toHaveLength(1);
expect(nodeToolCalls[0]?.[2]).toEqual(
expect.objectContaining({
stream: "tool",
data: expect.objectContaining({
phase: "start",
name: "read",
}),
}),
);
const payload = requireRecord(nodeToolCalls[0]?.[2], "node tool payload");
expect(payload.stream).toBe("tool");
expectRecordFields(requireRecord(payload.data, "node tool payload data"), {
phase: "start",
name: "read",
});
resetAgentRunContextForTest();
});
@@ -897,32 +932,31 @@ describe("agent event handler", () => {
});
expect(broadcastToConnIds).toHaveBeenCalledTimes(1);
expect(broadcastToConnIds).toHaveBeenCalledWith(
"session.tool",
expect.objectContaining({
runId: "run-session-tool",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
data: expect.objectContaining({
phase: "start",
name: "exec",
toolCallId: "tool-session-1",
args: { command: "echo hi" },
}),
}),
new Set(["conn-session"]),
{ dropIfSlow: true },
);
expect(broadcastToConnIds.mock.calls[0]?.[0]).toBe("session.tool");
const sessionToolPayload = requireMockPayload(broadcastToConnIds, 0, 1, "session tool payload");
expectRecordFields(sessionToolPayload, {
runId: "run-session-tool",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
});
expectRecordFields(requireRecord(sessionToolPayload.data, "session tool payload data"), {
phase: "start",
name: "exec",
toolCallId: "tool-session-1",
args: { command: "echo hi" },
});
expect(broadcastToConnIds.mock.calls[0]?.[2]).toEqual(new Set(["conn-session"]));
expect(broadcastToConnIds.mock.calls[0]?.[3]).toEqual({ dropIfSlow: true });
resetAgentRunContextForTest();
});
@@ -963,31 +997,30 @@ describe("agent event handler", () => {
});
expect(broadcastToConnIds).toHaveBeenCalledTimes(1);
expect(broadcastToConnIds).toHaveBeenCalledWith(
"agent",
expect.objectContaining({
runId: "run-tool-owner",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
data: expect.objectContaining({
phase: "start",
name: "exec",
toolCallId: "tool-run-1",
args: { command: "echo hi" },
}),
}),
new Set(["conn-run"]),
);
expect(broadcastToConnIds.mock.calls[0]?.[0]).toBe("agent");
const runToolPayload = requireMockPayload(broadcastToConnIds, 0, 1, "run tool payload");
expectRecordFields(runToolPayload, {
runId: "run-tool-owner",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
});
expectRecordFields(requireRecord(runToolPayload.data, "run tool payload data"), {
phase: "start",
name: "exec",
toolCallId: "tool-run-1",
args: { command: "echo hi" },
});
expect(broadcastToConnIds.mock.calls[0]?.[2]).toEqual(new Set(["conn-run"]));
resetAgentRunContextForTest();
});
@@ -1026,31 +1059,30 @@ describe("agent event handler", () => {
},
});
expect(nodeSendToSession).toHaveBeenCalledWith(
"session-1",
"agent",
expect.objectContaining({
runId: "run-tool-node",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
data: expect.objectContaining({
phase: "start",
name: "exec",
toolCallId: "tool-node-1",
args: { command: "echo hi" },
}),
}),
);
expect(nodeSendToSession.mock.calls[0]?.[0]).toBe("session-1");
expect(nodeSendToSession.mock.calls[0]?.[1]).toBe("agent");
const nodeToolPayload = requireMockPayload(nodeSendToSession, 0, 2, "node tool payload");
expectRecordFields(nodeToolPayload, {
runId: "run-tool-node",
sessionKey: "session-1",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
lastThreadId: 42,
fastMode: true,
verboseLevel: "on",
stream: "tool",
ts: 1_234,
});
expectRecordFields(requireRecord(nodeToolPayload.data, "node tool payload data"), {
phase: "start",
name: "exec",
toolCallId: "tool-node-1",
args: { command: "echo hi" },
});
resetAgentRunContextForTest();
});
@@ -1091,25 +1123,29 @@ describe("agent event handler", () => {
([event]) => event === "sessions.changed",
);
expect(sessionsChangedCalls).toHaveLength(2);
expect(sessionsChangedCalls[1]?.[1]).toEqual(
expect.objectContaining({
sessionKey: "session-finished",
phase: "end",
status: "done",
startedAt: 900,
endedAt: 1_700,
runtimeMs: 800,
updatedAt: 1_700,
abortedLastRun: false,
}),
);
expect(persistGatewaySessionLifecycleEventMock).toHaveBeenCalledWith({
expectPayloadFields(sessionsChangedCalls[1]?.[1], {
sessionKey: "session-finished",
event: expect.objectContaining({
runId: "run-finished",
data: expect.objectContaining({ phase: "end" }),
}),
phase: "end",
status: "done",
startedAt: 900,
endedAt: 1_700,
runtimeMs: 800,
updatedAt: 1_700,
abortedLastRun: false,
});
const persistParams = requireRecord(
persistGatewaySessionLifecycleEventMock.mock.calls
.map((call) => call[0])
.find((params) => {
const event = (params as { event?: { data?: { phase?: string } } } | undefined)?.event;
return event?.data?.phase === "end";
}),
"persist lifecycle params",
);
expect(persistParams.sessionKey).toBe("session-finished");
const persistEvent = requireRecord(persistParams.event, "persist lifecycle event");
expect(persistEvent.runId).toBe("run-finished");
expect(requireRecord(persistEvent.data, "persist lifecycle event data").phase).toBe("end");
resetAgentRunContextForTest();
});
@@ -1182,30 +1218,28 @@ describe("agent event handler", () => {
},
});
expect(broadcastToConnIds).toHaveBeenCalledWith(
"sessions.changed",
expect.objectContaining({
sessionKey: "session-finished",
phase: "end",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
fastMode: true,
sendPolicy: "deny",
verboseLevel: "on",
responseUsage: "full",
totalTokens: 42,
totalTokensFresh: true,
contextTokens: 21,
estimatedCostUsd: 0.12,
lastThreadId: 42,
}),
new Set(["conn-session"]),
{ dropIfSlow: true },
);
expect(broadcastToConnIds.mock.calls[0]?.[0]).toBe("sessions.changed");
expectPayloadFields(broadcastToConnIds.mock.calls[0]?.[1], {
sessionKey: "session-finished",
phase: "end",
spawnedBy: "agent:main:main",
spawnedWorkspaceDir: "/tmp/subagent",
forkedFromParent: true,
spawnDepth: 2,
subagentRole: "orchestrator",
subagentControlScope: "children",
fastMode: true,
sendPolicy: "deny",
verboseLevel: "on",
responseUsage: "full",
totalTokens: 42,
totalTokensFresh: true,
contextTokens: 21,
estimatedCostUsd: 0.12,
lastThreadId: 42,
});
expect(broadcastToConnIds.mock.calls[0]?.[2]).toEqual(new Set(["conn-session"]));
expect(broadcastToConnIds.mock.calls[0]?.[3]).toEqual({ dropIfSlow: true });
});
it("keeps tool output for Control UI recipients when verbose is on", () => {
@@ -1507,7 +1541,7 @@ describe("agent event handler", () => {
([, payload]) => (payload as { state?: string }).state === "error",
);
expect(chatErrors).toHaveLength(1);
expect(chatErrors[0]?.[1]).toMatchObject({
expectPayloadFields(chatErrors[0]?.[1], {
runId: "run-chat-send",
sessionKey: "session-chat-send",
state: "error",
@@ -1540,13 +1574,14 @@ describe("agent event handler", () => {
expect(chatBroadcastCalls(broadcast)).toHaveLength(0);
expect(broadcast.mock.calls.some(([event]) => event === "agent")).toBe(false);
expect(nodeSendToSession).not.toHaveBeenCalled();
expect(persistGatewaySessionLifecycleEventMock).toHaveBeenCalledWith({
sessionKey: "session-hidden",
event: expect.objectContaining({
runId: "run-hidden",
data: expect.objectContaining({ phase: "end" }),
}),
});
const persistParams = requireRecord(
persistGatewaySessionLifecycleEventMock.mock.calls[0]?.[0],
"persist lifecycle params",
);
expect(persistParams.sessionKey).toBe("session-hidden");
const persistEvent = requireRecord(persistParams.event, "persist lifecycle event");
expect(persistEvent.runId).toBe("run-hidden");
expect(requireRecord(persistEvent.data, "persist lifecycle event data").phase).toBe("end");
});
it("uses agent event sessionKey when run-context lookup cannot resolve", () => {
@@ -1697,7 +1732,7 @@ describe("agent event handler", () => {
const chatCalls = chatBroadcastCalls(broadcast);
expect(chatCalls.length).toBeGreaterThanOrEqual(1);
const [, payload] = chatCalls[0];
expect(payload).toMatchObject({
expectPayloadFields(payload, {
sessionKey: "agent:coder:subagent:abc",
spawnedBy: "agent:conductor:task:parent-1",
state: "delta",
@@ -1705,7 +1740,7 @@ describe("agent event handler", () => {
const nodeCalls = sessionChatCalls(nodeSendToSession);
expect(nodeCalls.length).toBeGreaterThanOrEqual(1);
expect(nodeCalls[0][2]).toMatchObject({
expectPayloadFields(nodeCalls[0]?.[2], {
spawnedBy: "agent:conductor:task:parent-1",
});
});
@@ -1748,7 +1783,7 @@ describe("agent event handler", () => {
chatCalls.find(([, p]) => p.state === "final"),
"final chat call",
);
expect(finalCall[1]).toMatchObject({
expectPayloadFields(finalCall[1], {
sessionKey: "agent:coder:subagent:abc",
spawnedBy: "agent:conductor:task:parent-1",
state: "final",
@@ -1839,7 +1874,7 @@ describe("agent event handler", () => {
const agentCalls = broadcast.mock.calls.filter(([event]) => event === "agent");
expect(agentCalls.length).toBeGreaterThanOrEqual(1);
expect(agentCalls[0][1]).toMatchObject({
expectPayloadFields(agentCalls[0]?.[1], {
sessionKey: "agent:coder:subagent:xyz",
spawnedBy: "agent:conductor:task:parent-2",
});
@@ -1886,7 +1921,7 @@ describe("agent event handler", () => {
chatCalls.find(([, p]) => p.state === "error"),
"error chat call",
);
expect(errorCall[1]).toMatchObject({
expectPayloadFields(errorCall[1], {
sessionKey: "agent:coder:subagent:err",
spawnedBy: "agent:conductor:task:parent-err",
state: "error",
@@ -1951,7 +1986,7 @@ describe("agent event handler", () => {
),
"flushed delta chat call",
);
expect(flushedDelta[1]).toMatchObject({
expectPayloadFields(flushedDelta[1], {
spawnedBy: "agent:conductor:task:parent-flush",
});
@@ -1994,11 +2029,11 @@ describe("agent event handler", () => {
agentCalls.find(([, p]) => p.stream === "error" && p.data?.reason === "seq gap"),
"seq gap error agent call",
);
expect(gapError[1]).toMatchObject({
expectPayloadFields(gapError[1], {
sessionKey: "agent:coder:subagent:gap",
spawnedBy: "agent:conductor:task:parent-gap",
data: { reason: "seq gap", expected: 2, received: 5 },
});
expectPayloadDataFields(gapError[1], { reason: "seq gap", expected: 2, received: 5 });
resetAgentRunContextForTest();
});
@@ -2051,7 +2086,7 @@ describe("agent event handler", () => {
// All broadcasts still have correct spawnedBy
const chatCalls = chatBroadcastCalls(broadcast);
for (const [, payload] of chatCalls) {
expect(payload).toMatchObject({
expectPayloadFields(payload, {
spawnedBy: "agent:conductor:task:parent-cache",
});
}