Files
openclaw/src/gateway/session-utils.fs.test.ts
2026-02-18 17:01:22 +00:00

747 lines
27 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
import {
archiveSessionTranscripts,
readFirstUserMessageFromTranscript,
readLastMessagePreviewFromTranscript,
readSessionMessages,
readSessionTitleFieldsFromTranscript,
readSessionPreviewItemsFromTranscript,
resolveSessionTranscriptCandidates,
} from "./session-utils.fs.js";
function registerTempSessionStore(
prefix: string,
assignPaths: (tmpDir: string, storePath: string) => void,
) {
let dir = "";
beforeAll(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
assignPaths(dir, path.join(dir, "sessions.json"));
});
afterAll(() => {
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
}
describe("readFirstUserMessageFromTranscript", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-session-fs-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
test("returns null when transcript file does not exist", () => {
const result = readFirstUserMessageFromTranscript("nonexistent-session", storePath);
expect(result).toBeNull();
});
test("returns first user message from transcript with string content", () => {
const sessionId = "test-session-1";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "Hello world" } }),
JSON.stringify({ message: { role: "assistant", content: "Hi there" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Hello world");
});
test("returns first user message from transcript with array content", () => {
const sessionId = "test-session-2";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({
message: {
role: "user",
content: [{ type: "text", text: "Array message content" }],
},
}),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Array message content");
});
test("returns first user message from transcript with input_text content", () => {
const sessionId = "test-session-2b";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({
message: {
role: "user",
content: [{ type: "input_text", text: "Input text content" }],
},
}),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Input text content");
});
test("skips non-user messages to find first user message", () => {
const sessionId = "test-session-3";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "system", content: "System prompt" } }),
JSON.stringify({ message: { role: "assistant", content: "Greeting" } }),
JSON.stringify({ message: { role: "user", content: "First user question" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("First user question");
});
test("skips inter-session user messages by default", () => {
const sessionId = "test-session-inter-session";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({
message: {
role: "user",
content: "Forwarded by session tool",
provenance: { kind: "inter_session", sourceTool: "sessions_send" },
},
}),
JSON.stringify({
message: { role: "user", content: "Real user message" },
}),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Real user message");
});
test("returns null when no user messages exist", () => {
const sessionId = "test-session-4";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "system", content: "System prompt" } }),
JSON.stringify({ message: { role: "assistant", content: "Greeting" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBeNull();
});
test("handles malformed JSON lines gracefully", () => {
const sessionId = "test-session-5";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
"not valid json",
JSON.stringify({ message: { role: "user", content: "Valid message" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Valid message");
});
test("uses sessionFile parameter when provided", () => {
const sessionId = "test-session-6";
const customPath = path.join(tmpDir, "custom-transcript.jsonl");
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "Custom file message" } }),
];
fs.writeFileSync(customPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath, customPath);
expect(result).toBe("Custom file message");
});
test("trims whitespace from message content", () => {
const sessionId = "test-session-7";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [JSON.stringify({ message: { role: "user", content: " Padded message " } })];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Padded message");
});
test("returns null for empty content", () => {
const sessionId = "test-session-8";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "user", content: "" } }),
JSON.stringify({ message: { role: "user", content: "Second message" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readFirstUserMessageFromTranscript(sessionId, storePath);
expect(result).toBe("Second message");
});
});
describe("readLastMessagePreviewFromTranscript", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-session-fs-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
test("returns null when transcript file does not exist", () => {
const result = readLastMessagePreviewFromTranscript("nonexistent-session", storePath);
expect(result).toBeNull();
});
test("returns null for empty file", () => {
const sessionId = "test-last-empty";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
fs.writeFileSync(transcriptPath, "", "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBeNull();
});
test("returns last user message from transcript", () => {
const sessionId = "test-last-user";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "user", content: "First user" } }),
JSON.stringify({ message: { role: "assistant", content: "First assistant" } }),
JSON.stringify({ message: { role: "user", content: "Last user message" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Last user message");
});
test("returns last assistant message from transcript", () => {
const sessionId = "test-last-assistant";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "user", content: "User question" } }),
JSON.stringify({ message: { role: "assistant", content: "Final assistant reply" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Final assistant reply");
});
test("skips system messages to find last user/assistant", () => {
const sessionId = "test-last-skip-system";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "user", content: "Real last" } }),
JSON.stringify({ message: { role: "system", content: "System at end" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Real last");
});
test("returns null when no user/assistant messages exist", () => {
const sessionId = "test-last-no-match";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "system", content: "Only system" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBeNull();
});
test("handles malformed JSON lines gracefully", () => {
const sessionId = "test-last-malformed";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "user", content: "Valid first" } }),
"not valid json at end",
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Valid first");
});
test("handles array content format", () => {
const sessionId = "test-last-array";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({
message: {
role: "assistant",
content: [{ type: "text", text: "Array content response" }],
},
}),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Array content response");
});
test("handles output_text content format", () => {
const sessionId = "test-last-output-text";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({
message: {
role: "assistant",
content: [{ type: "output_text", text: "Output text response" }],
},
}),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Output text response");
});
test("uses sessionFile parameter when provided", () => {
const sessionId = "test-last-custom";
const customPath = path.join(tmpDir, "custom-last.jsonl");
const lines = [JSON.stringify({ message: { role: "user", content: "Custom file last" } })];
fs.writeFileSync(customPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath, customPath);
expect(result).toBe("Custom file last");
});
test("trims whitespace from message content", () => {
const sessionId = "test-last-trim";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "assistant", content: " Padded response " } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Padded response");
});
test("skips empty content to find previous message", () => {
const sessionId = "test-last-skip-empty";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ message: { role: "assistant", content: "Has content" } }),
JSON.stringify({ message: { role: "user", content: "" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Has content");
});
test("reads from end of large file (16KB window)", () => {
const sessionId = "test-last-large";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const padding = JSON.stringify({ message: { role: "user", content: "x".repeat(500) } });
const lines: string[] = [];
for (let i = 0; i < 30; i++) {
lines.push(padding);
}
lines.push(JSON.stringify({ message: { role: "assistant", content: "Last in large file" } }));
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Last in large file");
});
test("handles valid UTF-8 content", () => {
const sessionId = "test-last-utf8";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const validLine = JSON.stringify({
message: { role: "user", content: "Valid UTF-8: 你好世界 🌍" },
});
fs.writeFileSync(transcriptPath, validLine, "utf-8");
const result = readLastMessagePreviewFromTranscript(sessionId, storePath);
expect(result).toBe("Valid UTF-8: 你好世界 🌍");
});
});
describe("readSessionTitleFieldsFromTranscript cache", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-session-fs-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
test("returns cached values without re-reading when unchanged", () => {
const sessionId = "test-cache-1";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "Hello world" } }),
JSON.stringify({ message: { role: "assistant", content: "Hi there" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const readSpy = vi.spyOn(fs, "readSync");
const first = readSessionTitleFieldsFromTranscript(sessionId, storePath);
const readsAfterFirst = readSpy.mock.calls.length;
expect(readsAfterFirst).toBeGreaterThan(0);
const second = readSessionTitleFieldsFromTranscript(sessionId, storePath);
expect(second).toEqual(first);
expect(readSpy.mock.calls.length).toBe(readsAfterFirst);
readSpy.mockRestore();
});
test("invalidates cache when transcript changes", () => {
const sessionId = "test-cache-2";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "First" } }),
JSON.stringify({ message: { role: "assistant", content: "Old" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const readSpy = vi.spyOn(fs, "readSync");
const first = readSessionTitleFieldsFromTranscript(sessionId, storePath);
const readsAfterFirst = readSpy.mock.calls.length;
expect(first.lastMessagePreview).toBe("Old");
fs.appendFileSync(
transcriptPath,
`\n${JSON.stringify({ message: { role: "assistant", content: "New" } })}`,
"utf-8",
);
const second = readSessionTitleFieldsFromTranscript(sessionId, storePath);
expect(second.lastMessagePreview).toBe("New");
expect(readSpy.mock.calls.length).toBeGreaterThan(readsAfterFirst);
readSpy.mockRestore();
});
});
describe("readSessionMessages", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-session-fs-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
test("includes synthetic compaction markers for compaction entries", () => {
const sessionId = "test-session-compaction";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "Hello" } }),
JSON.stringify({
type: "compaction",
id: "comp-1",
timestamp: "2026-02-07T00:00:00.000Z",
summary: "Compacted history",
firstKeptEntryId: "x",
tokensBefore: 123,
}),
JSON.stringify({ message: { role: "assistant", content: "World" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const out = readSessionMessages(sessionId, storePath);
expect(out).toHaveLength(3);
const marker = out[1] as {
role: string;
content?: Array<{ text?: string }>;
__openclaw?: { kind?: string; id?: string };
timestamp?: number;
};
expect(marker.role).toBe("system");
expect(marker.content?.[0]?.text).toBe("Compaction");
expect(marker.__openclaw?.kind).toBe("compaction");
expect(marker.__openclaw?.id).toBe("comp-1");
expect(typeof marker.timestamp).toBe("number");
});
test("reads cross-agent absolute sessionFile when storePath points to another agent dir", () => {
const sessionId = "cross-agent-default-root";
const sessionFile = path.join(tmpDir, "agents", "ops", "sessions", `${sessionId}.jsonl`);
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
fs.writeFileSync(
sessionFile,
[
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "from-ops" } }),
].join("\n"),
"utf-8",
);
const wrongStorePath = path.join(tmpDir, "agents", "main", "sessions", "sessions.json");
const out = readSessionMessages(sessionId, wrongStorePath, sessionFile);
expect(out).toEqual([{ role: "user", content: "from-ops" }]);
});
test("reads cross-agent absolute sessionFile for custom per-agent store roots", () => {
const sessionId = "cross-agent-custom-root";
const sessionFile = path.join(
tmpDir,
"custom",
"agents",
"ops",
"sessions",
`${sessionId}.jsonl`,
);
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
fs.writeFileSync(
sessionFile,
[
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "assistant", content: "from-custom-ops" } }),
].join("\n"),
"utf-8",
);
const wrongStorePath = path.join(
tmpDir,
"custom",
"agents",
"main",
"sessions",
"sessions.json",
);
const out = readSessionMessages(sessionId, wrongStorePath, sessionFile);
expect(out).toEqual([{ role: "assistant", content: "from-custom-ops" }]);
});
});
describe("readSessionPreviewItemsFromTranscript", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-session-preview-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
function writeTranscriptLines(sessionId: string, lines: string[]) {
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
}
function readPreview(sessionId: string, maxItems = 3, maxChars = 120) {
return readSessionPreviewItemsFromTranscript(
sessionId,
storePath,
undefined,
undefined,
maxItems,
maxChars,
);
}
test("returns recent preview items with tool summary", () => {
const sessionId = "preview-session";
const lines = createToolSummaryPreviewTranscriptLines(sessionId);
writeTranscriptLines(sessionId, lines);
const result = readPreview(sessionId);
expect(result.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
expect(result[1]?.text).toContain("call weather");
});
test("detects tool calls from tool_use/tool_call blocks and toolName field", () => {
const sessionId = "preview-session-tools";
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "assistant", content: "Hi" } }),
JSON.stringify({
message: {
role: "assistant",
toolName: "camera",
content: [
{ type: "tool_use", name: "read" },
{ type: "tool_call", name: "write" },
],
},
}),
JSON.stringify({ message: { role: "assistant", content: "Done" } }),
];
writeTranscriptLines(sessionId, lines);
const result = readPreview(sessionId);
expect(result.map((item) => item.role)).toEqual(["assistant", "tool", "assistant"]);
expect(result[1]?.text).toContain("call");
expect(result[1]?.text).toContain("camera");
expect(result[1]?.text).toContain("read");
// Preview text may not list every tool name; it should at least hint there were multiple calls.
expect(result[1]?.text).toMatch(/\+\d+/);
});
test("truncates preview text to max chars", () => {
const sessionId = "preview-truncate";
const longText = "a".repeat(60);
const lines = [JSON.stringify({ message: { role: "assistant", content: longText } })];
writeTranscriptLines(sessionId, lines);
const result = readPreview(sessionId, 1, 24);
expect(result).toHaveLength(1);
expect(result[0]?.text.length).toBe(24);
expect(result[0]?.text.endsWith("...")).toBe(true);
});
});
describe("resolveSessionTranscriptCandidates", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
test("fallback candidate uses OPENCLAW_HOME instead of os.homedir()", () => {
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
vi.stubEnv("HOME", "/home/other");
const candidates = resolveSessionTranscriptCandidates("sess-1", undefined);
const fallback = candidates[candidates.length - 1];
expect(fallback).toBe(
path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "sessions", "sess-1.jsonl"),
);
});
});
describe("resolveSessionTranscriptCandidates safety", () => {
test("keeps cross-agent absolute sessionFile when storePath agent context differs", () => {
const storePath = "/tmp/openclaw/agents/main/sessions/sessions.json";
const sessionFile = "/tmp/openclaw/agents/ops/sessions/sess-safe.jsonl";
const candidates = resolveSessionTranscriptCandidates("sess-safe", storePath, sessionFile);
expect(candidates.map((value) => path.resolve(value))).toContain(path.resolve(sessionFile));
});
test("keeps cross-agent absolute sessionFile for custom per-agent store roots", () => {
const storePath = "/srv/custom/agents/main/sessions/sessions.json";
const sessionFile = "/srv/custom/agents/ops/sessions/sess-safe.jsonl";
const candidates = resolveSessionTranscriptCandidates("sess-safe", storePath, sessionFile);
expect(candidates.map((value) => path.resolve(value))).toContain(path.resolve(sessionFile));
});
test("drops unsafe session IDs instead of producing traversal paths", () => {
const candidates = resolveSessionTranscriptCandidates(
"../etc/passwd",
"/tmp/openclaw/agents/main/sessions/sessions.json",
);
expect(candidates).toEqual([]);
});
test("drops unsafe sessionFile candidates and keeps safe fallbacks", () => {
const storePath = "/tmp/openclaw/agents/main/sessions/sessions.json";
const candidates = resolveSessionTranscriptCandidates(
"sess-safe",
storePath,
"../../etc/passwd",
);
const normalizedCandidates = candidates.map((value) => path.resolve(value));
const expectedFallback = path.resolve(path.dirname(storePath), "sess-safe.jsonl");
expect(candidates.some((value) => value.includes("etc/passwd"))).toBe(false);
expect(normalizedCandidates).toContain(expectedFallback);
});
});
describe("archiveSessionTranscripts", () => {
let tmpDir: string;
let storePath: string;
registerTempSessionStore("openclaw-archive-test-", (nextTmpDir, nextStorePath) => {
tmpDir = nextTmpDir;
storePath = nextStorePath;
});
beforeAll(() => {
vi.stubEnv("OPENCLAW_HOME", tmpDir);
});
afterAll(() => {
vi.unstubAllEnvs();
});
test("archives existing transcript file and returns archived path", () => {
const sessionId = "sess-archive-1";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
fs.writeFileSync(transcriptPath, '{"type":"session"}\n', "utf-8");
const archived = archiveSessionTranscripts({
sessionId,
storePath,
reason: "reset",
});
expect(archived).toHaveLength(1);
expect(archived[0]).toContain(".reset.");
expect(fs.existsSync(transcriptPath)).toBe(false);
expect(fs.existsSync(archived[0])).toBe(true);
});
test("archives transcript found via explicit sessionFile path", () => {
const sessionId = "sess-archive-2";
const customPath = path.join(tmpDir, "custom-transcript.jsonl");
fs.writeFileSync(customPath, '{"type":"session"}\n', "utf-8");
const archived = archiveSessionTranscripts({
sessionId,
storePath: undefined,
sessionFile: customPath,
reason: "reset",
});
expect(archived).toHaveLength(1);
expect(fs.existsSync(customPath)).toBe(false);
expect(fs.existsSync(archived[0])).toBe(true);
});
test("returns empty array when no transcript files exist", () => {
const archived = archiveSessionTranscripts({
sessionId: "nonexistent-session",
storePath,
reason: "reset",
});
expect(archived).toEqual([]);
});
test("skips files that do not exist and archives only existing ones", () => {
const sessionId = "sess-archive-3";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
fs.writeFileSync(transcriptPath, '{"type":"session"}\n', "utf-8");
const archived = archiveSessionTranscripts({
sessionId,
storePath,
sessionFile: "/nonexistent/path/file.jsonl",
reason: "deleted",
});
expect(archived).toHaveLength(1);
expect(archived[0]).toContain(".deleted.");
expect(fs.existsSync(transcriptPath)).toBe(false);
});
});