mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
test: clear gateway agent event broad matchers
This commit is contained in:
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user