Files
openclaw/src/auto-reply/reply/agent-runner-session-reset.test.ts
2026-04-09 05:58:45 +01:00

171 lines
5.8 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../../config/sessions.js";
import {
resetReplyRunSession,
setAgentRunnerSessionResetTestDeps,
} from "./agent-runner-session-reset.js";
import type { FollowupRun } from "./queue.js";
const refreshQueuedFollowupSessionMock = vi.fn();
const errorMock = vi.fn();
function createFollowupRun(): FollowupRun {
return {
prompt: "hello",
summaryLine: "hello",
enqueuedAt: Date.now(),
run: {
sessionId: "session",
sessionKey: "main",
messageProvider: "whatsapp",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: {},
skillsSnapshot: {},
provider: "anthropic",
model: "claude",
thinkLevel: "low",
verboseLevel: "off",
elevatedLevel: "off",
bashElevated: { enabled: false, allowed: false, defaultLevel: "off" },
timeoutMs: 1_000,
blockReplyBreak: "message_end",
},
} as unknown as FollowupRun;
}
async function writeSessionStore(
storePath: string,
sessionKey: string,
entry: SessionEntry,
): Promise<void> {
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: entry }, null, 2), "utf8");
}
describe("resetReplyRunSession", () => {
let rootDir = "";
beforeEach(async () => {
rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-reset-run-"));
refreshQueuedFollowupSessionMock.mockReset();
errorMock.mockReset();
setAgentRunnerSessionResetTestDeps({
generateSecureUuid: () => "00000000-0000-0000-0000-000000000123",
refreshQueuedFollowupSession: refreshQueuedFollowupSessionMock as never,
error: errorMock,
});
});
afterEach(async () => {
setAgentRunnerSessionResetTestDeps();
await fs.rm(rootDir, { recursive: true, force: true });
});
it("rotates the session and clears stale runtime and fallback fields", async () => {
const storePath = path.join(rootDir, "sessions.json");
const sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: 1,
sessionFile: path.join(rootDir, "session.jsonl"),
modelProvider: "qwencode",
model: "qwen",
contextTokens: 123,
fallbackNoticeSelectedModel: "anthropic/claude",
fallbackNoticeActiveModel: "openai/gpt",
fallbackNoticeReason: "rate limit",
systemPromptReport: {
source: "run",
generatedAt: 1,
systemPrompt: { chars: 1, projectContextChars: 0, nonProjectContextChars: 1 },
injectedWorkspaceFiles: [],
skills: { promptChars: 0, entries: [] },
tools: { listChars: 0, schemaChars: 0, entries: [] },
},
};
const sessionStore = { main: sessionEntry };
const followupRun = createFollowupRun();
await writeSessionStore(storePath, "main", sessionEntry);
let activeSessionEntry: SessionEntry | undefined = sessionEntry;
let isNewSession = false;
const reset = await resetReplyRunSession({
options: {
failureLabel: "compaction failure",
buildLogMessage: (next) => `reset ${next}`,
},
sessionKey: "main",
queueKey: "main",
activeSessionEntry,
activeSessionStore: sessionStore,
storePath,
followupRun,
onActiveSessionEntry: (entry) => {
activeSessionEntry = entry;
},
onNewSession: () => {
isNewSession = true;
},
});
expect(reset).toBe(true);
expect(isNewSession).toBe(true);
expect(activeSessionEntry?.sessionId).toBe("00000000-0000-0000-0000-000000000123");
expect(followupRun.run.sessionId).toBe(activeSessionEntry?.sessionId);
expect(activeSessionEntry?.modelProvider).toBeUndefined();
expect(activeSessionEntry?.model).toBeUndefined();
expect(activeSessionEntry?.contextTokens).toBeUndefined();
expect(activeSessionEntry?.fallbackNoticeSelectedModel).toBeUndefined();
expect(activeSessionEntry?.fallbackNoticeActiveModel).toBeUndefined();
expect(activeSessionEntry?.fallbackNoticeReason).toBeUndefined();
expect(activeSessionEntry?.systemPromptReport).toBeUndefined();
expect(refreshQueuedFollowupSessionMock).toHaveBeenCalledWith({
key: "main",
previousSessionId: "session",
nextSessionId: activeSessionEntry?.sessionId,
nextSessionFile: activeSessionEntry?.sessionFile,
});
expect(errorMock).toHaveBeenCalledWith("reset 00000000-0000-0000-0000-000000000123");
const persisted = JSON.parse(await fs.readFile(storePath, "utf8")) as {
main: SessionEntry;
};
expect(persisted.main.sessionId).toBe(activeSessionEntry?.sessionId);
expect(persisted.main.fallbackNoticeReason).toBeUndefined();
});
it("cleans up the old transcript when requested", async () => {
const storePath = path.join(rootDir, "sessions.json");
const oldTranscriptPath = path.join(rootDir, "old-session.jsonl");
await fs.writeFile(oldTranscriptPath, "old", "utf8");
const sessionEntry: SessionEntry = {
sessionId: "old-session",
updatedAt: 1,
sessionFile: oldTranscriptPath,
};
const sessionStore = { main: sessionEntry };
await writeSessionStore(storePath, "main", sessionEntry);
await resetReplyRunSession({
options: {
failureLabel: "role ordering conflict",
cleanupTranscripts: true,
buildLogMessage: (next) => `reset ${next}`,
},
sessionKey: "main",
queueKey: "main",
activeSessionEntry: sessionEntry,
activeSessionStore: sessionStore,
storePath,
followupRun: createFollowupRun(),
onActiveSessionEntry: () => {},
onNewSession: () => {},
});
await expect(fs.access(oldTranscriptPath)).rejects.toThrow();
});
});