openclaw / src /agents /subagents /spawn /subagent-spawn-cleanup.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4c76b0d verified
Raw History Blame Contribute Delete
4.74 kB
import { describe, expect, it, vi } from "vitest";
import { withPluginRuntimeGatewayRequestScope } from "../../../plugins/runtime/gateway-request-scope.js";
import {
cleanupProvisionalSession,
terminateAcceptedCollectorRun,
} from "./subagent-spawn-cleanup.js";
function sessionChangedError(): Error {
return Object.assign(new Error("session changed"), {
name: "GatewayClientRequestError",
gatewayCode: "INVALID_REQUEST",
details: { reason: "session-changed" },
});
}
describe("subagent spawn cleanup identity", () => {
it("requires both frozen session identities before deletion", async () => {
const callGateway = vi.fn();
await expect(
cleanupProvisionalSession("agent:main:subagent:child", {
expectedSessionId: "session-id",
callGateway,
}),
).resolves.toBe(false);
expect(callGateway).not.toHaveBeenCalled();
});
it("accepts chat.abort only when it confirms the exact run", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({ ok: true, aborted: false, runIds: [] })
.mockResolvedValueOnce({ deleted: true });
await terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
expectedSessionId: "session-id",
expectedLifecycleRevision: "session-revision",
callGateway,
});
expect(callGateway).toHaveBeenNthCalledWith(2, {
method: "sessions.delete",
params: {
key: "agent:main:subagent:child",
emitLifecycleHooks: false,
deleteTranscript: true,
expectedSessionId: "session-id",
expectedLifecycleRevision: "session-revision",
},
timeoutMs: 60_000,
});
});
it("does not delete after chat.abort confirms the matching run", async () => {
const callGateway = vi.fn(async () => ({
ok: true,
aborted: true,
runIds: ["gateway-run"],
}));
await terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
expectedSessionId: "session-id",
expectedLifecycleRevision: "session-revision",
callGateway,
});
expect(callGateway).toHaveBeenCalledOnce();
});
it("stops without deleting a durable session when the accepted run already ended", async () => {
const callGateway = vi.fn(async () => ({
ok: true,
aborted: false,
runIds: [],
}));
await terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
sessionCleanup: "preserve",
callGateway,
});
expect(callGateway).toHaveBeenCalledOnce();
});
it("retries abort without deleting a durable session after a gateway error", async () => {
const callGateway = vi
.fn()
.mockRejectedValueOnce(new Error("gateway unavailable"))
.mockResolvedValueOnce({ ok: true, aborted: false, runIds: [] });
await terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
sessionCleanup: "preserve",
callGateway,
});
expect(callGateway).toHaveBeenCalledTimes(2);
expect(callGateway).toHaveBeenNthCalledWith(2, {
method: "chat.abort",
params: {
sessionKey: "agent:main:subagent:child",
runId: "gateway-run",
},
timeoutMs: 60_000,
});
});
it("stops accepted-run cleanup when its Gateway request owner is retired", async () => {
const callGateway = vi
.fn()
.mockRejectedValueOnce(new Error("Gateway request owner is retired"))
.mockResolvedValue({ aborted: false, runIds: [] });
await withPluginRuntimeGatewayRequestScope(
{ resolveGatewayContext: () => undefined, isWebchatConnect: () => false },
() =>
terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
sessionCleanup: "preserve",
callGateway,
}),
);
expect(callGateway).toHaveBeenCalledOnce();
});
it("stops cleanup when guarded deletion observes a successor lifecycle", async () => {
const callGateway = vi
.fn()
.mockResolvedValueOnce({ ok: true, aborted: true, runIds: ["different-run"] })
.mockRejectedValueOnce(sessionChangedError());
await expect(
terminateAcceptedCollectorRun({
childSessionKey: "agent:main:subagent:child",
gatewayRunId: "gateway-run",
expectedSessionId: "session-id",
expectedLifecycleRevision: "session-revision",
callGateway,
}),
).resolves.toBeUndefined();
expect(callGateway).toHaveBeenCalledTimes(2);
});
});