
























@@ -1,16 +1,20 @@
11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { afterEach, describe, expect, it } from "vitest";
4+import { afterEach, describe, expect, it, vi } from "vitest";
55import {
66onSessionTranscriptUpdate,
77type SessionTranscriptUpdate,
88} from "../sessions/transcript-events.js";
9-import { archiveFileOnDisk } from "./session-transcript-files.fs.js";
9+import {
10+archiveFileOnDisk,
11+archiveSessionTranscriptsDetailed,
12+} from "./session-transcript-files.fs.js";
10131114const subscriptions: Array<() => void> = [];
12151316afterEach(() => {
17+vi.restoreAllMocks();
1418while (subscriptions.length > 0) {
1519subscriptions.pop()?.();
1620}
@@ -65,3 +69,113 @@ describe("archiveFileOnDisk transcript updates", () => {
6569}
6670});
6771});
72+73+describe("archiveSessionTranscriptsDetailed failure surface", () => {
74+it("invokes onArchiveError when fs.renameSync fails and returns only successful entries", () => {
75+const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-archive-failure-"));
76+try {
77+const sessionId = "11111111-1111-4111-8111-111111111111";
78+const sessionFile = path.join(tmpDir, `${sessionId}.jsonl`);
79+fs.writeFileSync(sessionFile, '{"type":"session-meta","agentId":"main"}\n');
80+81+const renameError = Object.assign(new Error("EACCES: permission denied"), {
82+code: "EACCES",
83+});
84+const renameSpy = vi.spyOn(fs, "renameSync").mockImplementation(() => {
85+throw renameError;
86+});
87+88+const errors: Array<{ err: unknown; sourcePath: string }> = [];
89+const archived = archiveSessionTranscriptsDetailed({
90+ sessionId,
91+storePath: path.join(tmpDir, "store.json"),
92+ sessionFile,
93+agentId: "main",
94+reason: "reset",
95+onArchiveError: (err, sourcePath) => {
96+errors.push({ err, sourcePath });
97+},
98+});
99+100+renameSpy.mockRestore();
101+102+expect(archived).toEqual([]);
103+expect(errors.length).toBeGreaterThan(0);
104+expect(errors[0].err).toBe(renameError);
105+expect(fs.existsSync(sessionFile)).toBe(true);
106+} finally {
107+fs.rmSync(tmpDir, { recursive: true, force: true });
108+}
109+});
110+111+it("archives normally when no onArchiveError is provided", () => {
112+const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-archive-success-"));
113+try {
114+const sessionId = "22222222-2222-4222-8222-222222222222";
115+const sessionFile = path.join(tmpDir, `${sessionId}.jsonl`);
116+fs.writeFileSync(sessionFile, '{"type":"session-meta","agentId":"main"}\n');
117+118+const archived = archiveSessionTranscriptsDetailed({
119+ sessionId,
120+storePath: path.join(tmpDir, "store.json"),
121+ sessionFile,
122+agentId: "main",
123+reason: "reset",
124+});
125+126+expect(archived.length).toBe(1);
127+expect(archived[0].archivedPath).toContain(".jsonl.reset.");
128+expect(fs.existsSync(archived[0].archivedPath)).toBe(true);
129+expect(fs.existsSync(sessionFile)).toBe(false);
130+} finally {
131+fs.rmSync(tmpDir, { recursive: true, force: true });
132+}
133+});
134+135+it("surfaces real chmod archive failures through onArchiveError", () => {
136+if (process.platform === "win32" || process.getuid?.() === 0) {
137+return;
138+}
139+140+const tmpDir = fs.realpathSync(
141+fs.mkdtempSync(path.join(os.tmpdir(), "oc-archive-real-eacces-")),
142+);
143+try {
144+const sessionId = "33333333-3333-4333-8333-333333333333";
145+const sessionFile = path.join(tmpDir, `${sessionId}.jsonl`);
146+fs.writeFileSync(sessionFile, '{"type":"session-meta","agentId":"main"}\n');
147+fs.chmodSync(tmpDir, 0o555);
148+149+const errors: Array<{ code?: string; sourcePath: string }> = [];
150+let archived: ReturnType<typeof archiveSessionTranscriptsDetailed> = [];
151+try {
152+archived = archiveSessionTranscriptsDetailed({
153+ sessionId,
154+storePath: path.join(tmpDir, "store.json"),
155+ sessionFile,
156+agentId: "main",
157+reason: "reset",
158+onArchiveError: (err, sourcePath) => {
159+const code = (err as NodeJS.ErrnoException | undefined)?.code;
160+errors.push({ code, sourcePath });
161+},
162+});
163+} finally {
164+fs.chmodSync(tmpDir, 0o755);
165+}
166+167+expect(archived).toEqual([]);
168+expect(errors.length).toBeGreaterThan(0);
169+expect(errors[0].sourcePath).toBe(sessionFile);
170+expect(errors[0].code).toMatch(/^(EACCES|EPERM)$/);
171+expect(fs.existsSync(sessionFile)).toBe(true);
172+} finally {
173+try {
174+fs.chmodSync(tmpDir, 0o755);
175+} catch {
176+// Already restored.
177+}
178+fs.rmSync(tmpDir, { recursive: true, force: true });
179+}
180+});
181+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。