fix(gateway): abort active runs during sessions.reset (#16576)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 43da87f2df
Co-authored-by: Grynn <212880+Grynn@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Vishal Doshi
2026-02-15 04:12:33 +05:30
committed by GitHub
parent d8da642611
commit 3efb752124
3 changed files with 189 additions and 21 deletions

View File

@@ -597,4 +597,154 @@ describe("gateway server sessions", () => {
ws.close();
});
test("sessions.reset aborts active runs and clears queues", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
const storePath = path.join(dir, "sessions.json");
testState.sessionStorePath = storePath;
await fs.writeFile(
path.join(dir, "sess-main.jsonl"),
`${JSON.stringify({ role: "user", content: "hello" })}\n`,
"utf-8",
);
await writeSessionStore({
entries: {
main: { sessionId: "sess-main", updatedAt: Date.now() },
},
});
embeddedRunMock.activeIds.add("sess-main");
embeddedRunMock.waitResults.set("sess-main", true);
const { ws } = await openClient();
const reset = await rpcReq<{ ok: true; key: string; entry: { sessionId: string } }>(
ws,
"sessions.reset",
{
key: "main",
},
);
expect(reset.ok).toBe(true);
expect(reset.payload?.key).toBe("agent:main:main");
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
expect(sessionCleanupMocks.stopSubagentsForRequester).toHaveBeenCalledWith({
cfg: expect.any(Object),
requesterSessionKey: "agent:main:main",
});
expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1);
const clearedKeys = sessionCleanupMocks.clearSessionQueues.mock.calls[0]?.[0] as string[];
expect(clearedKeys).toEqual(expect.arrayContaining(["main", "agent:main:main", "sess-main"]));
expect(embeddedRunMock.abortCalls).toEqual(["sess-main"]);
expect(embeddedRunMock.waitCalls).toEqual(["sess-main"]);
ws.close();
});
test("sessions.reset returns unavailable when active run does not stop", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
const storePath = path.join(dir, "sessions.json");
testState.sessionStorePath = storePath;
await fs.writeFile(
path.join(dir, "sess-main.jsonl"),
`${JSON.stringify({ role: "user", content: "hello" })}\n`,
"utf-8",
);
await writeSessionStore({
entries: {
main: { sessionId: "sess-main", updatedAt: Date.now() },
},
});
embeddedRunMock.activeIds.add("sess-main");
embeddedRunMock.waitResults.set("sess-main", false);
const { ws } = await openClient();
const reset = await rpcReq(ws, "sessions.reset", {
key: "main",
});
expect(reset.ok).toBe(false);
expect(reset.error?.code).toBe("UNAVAILABLE");
expect(reset.error?.message ?? "").toMatch(/still active/i);
expect(sessionCleanupMocks.stopSubagentsForRequester).toHaveBeenCalledWith({
cfg: expect.any(Object),
requesterSessionKey: "agent:main:main",
});
expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1);
const clearedKeys = sessionCleanupMocks.clearSessionQueues.mock.calls[0]?.[0] as string[];
expect(clearedKeys).toEqual(expect.arrayContaining(["main", "agent:main:main", "sess-main"]));
expect(embeddedRunMock.abortCalls).toEqual(["sess-main"]);
expect(embeddedRunMock.waitCalls).toEqual(["sess-main"]);
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{ sessionId?: string }
>;
expect(store["agent:main:main"]?.sessionId).toBe("sess-main");
const filesAfterResetAttempt = await fs.readdir(dir);
expect(filesAfterResetAttempt.some((f) => f.startsWith("sess-main.jsonl.reset."))).toBe(false);
ws.close();
});
test("sessions.delete returns unavailable when active run does not stop", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-"));
const storePath = path.join(dir, "sessions.json");
testState.sessionStorePath = storePath;
await fs.writeFile(
path.join(dir, "sess-active.jsonl"),
`${JSON.stringify({ role: "user", content: "active" })}\n`,
"utf-8",
);
await writeSessionStore({
entries: {
"discord:group:dev": {
sessionId: "sess-active",
updatedAt: Date.now(),
},
},
});
embeddedRunMock.activeIds.add("sess-active");
embeddedRunMock.waitResults.set("sess-active", false);
const { ws } = await openClient();
const deleted = await rpcReq(ws, "sessions.delete", {
key: "discord:group:dev",
});
expect(deleted.ok).toBe(false);
expect(deleted.error?.code).toBe("UNAVAILABLE");
expect(deleted.error?.message ?? "").toMatch(/still active/i);
expect(sessionCleanupMocks.stopSubagentsForRequester).toHaveBeenCalledWith({
cfg: expect.any(Object),
requesterSessionKey: "agent:main:discord:group:dev",
});
expect(sessionCleanupMocks.clearSessionQueues).toHaveBeenCalledTimes(1);
const clearedKeys = sessionCleanupMocks.clearSessionQueues.mock.calls[0]?.[0] as string[];
expect(clearedKeys).toEqual(
expect.arrayContaining(["discord:group:dev", "agent:main:discord:group:dev", "sess-active"]),
);
expect(embeddedRunMock.abortCalls).toEqual(["sess-active"]);
expect(embeddedRunMock.waitCalls).toEqual(["sess-active"]);
const store = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{ sessionId?: string }
>;
expect(store["agent:main:discord:group:dev"]?.sessionId).toBe("sess-active");
const filesAfterDeleteAttempt = await fs.readdir(dir);
expect(filesAfterDeleteAttempt.some((f) => f.startsWith("sess-active.jsonl.deleted."))).toBe(
false,
);
ws.close();
});
});