mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 07:12:12 +00:00
test: tighten subagent spawn hook assertions
This commit is contained in:
@@ -54,6 +54,19 @@ function findGatewayRequest(method: string): GatewayRequest | undefined {
|
||||
return getGatewayRequests().find((request) => request.method === method);
|
||||
}
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
expect(value, label).toBeTypeOf("object");
|
||||
expect(value, label).not.toBeNull();
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function expectFields(value: unknown, expected: Record<string, unknown>, label = "object"): void {
|
||||
const record = requireRecord(value, label);
|
||||
for (const [key, expectedValue] of Object.entries(expected)) {
|
||||
expect(record[key], `${label}.${key}`).toEqual(expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
function setConfig(next: Record<string, unknown>) {
|
||||
hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next);
|
||||
}
|
||||
@@ -132,10 +145,14 @@ function expectThreadBindFailureCleanup(
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
expectSessionsDeleteWithoutAgentStart();
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: result.childSessionKey,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
expectFields(
|
||||
deleteCall?.params,
|
||||
{
|
||||
key: result.childSessionKey,
|
||||
emitLifecycleHooks: false,
|
||||
},
|
||||
"delete params",
|
||||
);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -215,7 +232,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
context: "isolated",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
|
||||
expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");
|
||||
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
|
||||
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -242,25 +259,37 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>,
|
||||
];
|
||||
expect(event).toMatchObject({
|
||||
runId: "run-1",
|
||||
agentId: "main",
|
||||
label: "research",
|
||||
mode: "session",
|
||||
requester: {
|
||||
expectFields(
|
||||
event,
|
||||
{
|
||||
runId: "run-1",
|
||||
agentId: "main",
|
||||
label: "research",
|
||||
mode: "session",
|
||||
threadRequested: true,
|
||||
},
|
||||
"spawned event",
|
||||
);
|
||||
expectFields(
|
||||
event.requester,
|
||||
{
|
||||
channel: "discord",
|
||||
accountId: "work",
|
||||
to: "channel:123",
|
||||
threadId: 456,
|
||||
},
|
||||
threadRequested: true,
|
||||
});
|
||||
"spawned requester",
|
||||
);
|
||||
expect(event.childSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));
|
||||
expect(ctx).toMatchObject({
|
||||
runId: "run-1",
|
||||
requesterSessionKey: "main",
|
||||
childSessionKey: event.childSessionKey,
|
||||
});
|
||||
expectFields(
|
||||
ctx,
|
||||
{
|
||||
runId: "run-1",
|
||||
requesterSessionKey: "main",
|
||||
childSessionKey: event.childSessionKey,
|
||||
},
|
||||
"spawned context",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits subagent_spawned with threadRequested=false when not requested", async () => {
|
||||
@@ -269,20 +298,28 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
agentTo: "channel:123",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
|
||||
expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");
|
||||
expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();
|
||||
expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1);
|
||||
const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
|
||||
Record<string, unknown>,
|
||||
];
|
||||
expect(event).toMatchObject({
|
||||
mode: "run",
|
||||
threadRequested: false,
|
||||
requester: {
|
||||
expectFields(
|
||||
event,
|
||||
{
|
||||
mode: "run",
|
||||
threadRequested: false,
|
||||
},
|
||||
"spawned event",
|
||||
);
|
||||
expectFields(
|
||||
event.requester,
|
||||
{
|
||||
channel: "discord",
|
||||
to: "channel:123",
|
||||
},
|
||||
});
|
||||
"spawned requester",
|
||||
);
|
||||
});
|
||||
|
||||
it("respects explicit mode=run when thread binding is requested", async () => {
|
||||
@@ -294,13 +331,17 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
context: "isolated",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });
|
||||
expectFields(result, { status: "accepted", runId: "run-1", mode: "run" }, "spawn result");
|
||||
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
|
||||
const event = getSpawnedEventCall();
|
||||
expect(event).toMatchObject({
|
||||
mode: "run",
|
||||
threadRequested: true,
|
||||
});
|
||||
expectFields(
|
||||
event,
|
||||
{
|
||||
mode: "run",
|
||||
threadRequested: true,
|
||||
},
|
||||
"spawned event",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns error when thread binding cannot be created", async () => {
|
||||
@@ -377,26 +418,34 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
context: "isolated",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "error" });
|
||||
expect(result.status).toBe("error");
|
||||
expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1);
|
||||
const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [
|
||||
Record<string, unknown>,
|
||||
];
|
||||
expect(event).toMatchObject({
|
||||
targetSessionKey: expect.stringMatching(/^agent:main:subagent:/),
|
||||
accountId: "work",
|
||||
targetKind: "subagent",
|
||||
reason: "spawn-failed",
|
||||
sendFarewell: true,
|
||||
outcome: "error",
|
||||
error: "Session failed to start",
|
||||
});
|
||||
expect(event.targetSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));
|
||||
expectFields(
|
||||
event,
|
||||
{
|
||||
accountId: "work",
|
||||
targetKind: "subagent",
|
||||
reason: "spawn-failed",
|
||||
sendFarewell: true,
|
||||
outcome: "error",
|
||||
error: "Session failed to start",
|
||||
},
|
||||
"ended event",
|
||||
);
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: event.targetSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
expectFields(
|
||||
deleteCall?.params,
|
||||
{
|
||||
key: event.targetSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: false,
|
||||
},
|
||||
"delete params",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => {
|
||||
@@ -411,15 +460,19 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
context: "isolated",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "error" });
|
||||
expect(result.status).toBe("error");
|
||||
expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled();
|
||||
const methods = getGatewayMethods();
|
||||
expect(methods).toContain("sessions.delete");
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: true,
|
||||
});
|
||||
expectFields(
|
||||
deleteCall?.params,
|
||||
{
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: true,
|
||||
},
|
||||
"delete params",
|
||||
);
|
||||
});
|
||||
|
||||
it("cleans up the provisional session when lineage patching fails after thread binding", async () => {
|
||||
@@ -464,10 +517,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
expect(methods).toContain("sessions.delete");
|
||||
expect(methods).not.toContain("agent");
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: result.childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: true,
|
||||
});
|
||||
expectFields(
|
||||
deleteCall?.params,
|
||||
{
|
||||
key: result.childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: true,
|
||||
},
|
||||
"delete params",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user