mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:40:43 +00:00
fix(core): avoid session export filename collisions (#77762)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
|
||||
const hoisted = await vi.hoisted(async () => {
|
||||
@@ -119,6 +120,10 @@ function makeParams(): HandleCommandsParams {
|
||||
}
|
||||
|
||||
describe("buildExportSessionReply", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hoisted.resolveDefaultSessionStorePathMock.mockReturnValue("/tmp/target-store/sessions.json");
|
||||
@@ -231,6 +236,32 @@ describe("buildExportSessionReply", () => {
|
||||
expect(html).toContain('const base64 = document.getElementById("session-data").textContent;');
|
||||
});
|
||||
|
||||
it("suffixes colliding default export filenames instead of overwriting", async () => {
|
||||
const { buildExportSessionReply } = await import("./commands-export-session.js");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-05-05T10:11:12.345Z"));
|
||||
const collision = Object.assign(new Error("exists"), { code: "EEXIST" });
|
||||
hoisted.writeFileMock.mockRejectedValueOnce(collision).mockResolvedValueOnce(undefined);
|
||||
|
||||
const reply = await buildExportSessionReply(makeParams());
|
||||
|
||||
const expectedBase = path.join(
|
||||
"/tmp/workspace",
|
||||
"openclaw-session-session--2026-05-05T10-11-12.html",
|
||||
);
|
||||
const expectedSuffix = path.join(
|
||||
"/tmp/workspace",
|
||||
"openclaw-session-session--2026-05-05T10-11-12-2.html",
|
||||
);
|
||||
expect(hoisted.writeFileMock.mock.calls[0]?.[0]).toBe(expectedBase);
|
||||
expect(hoisted.writeFileMock.mock.calls[0]?.[2]).toMatchObject({
|
||||
encoding: "utf-8",
|
||||
flag: "wx",
|
||||
});
|
||||
expect(hoisted.writeFileMock.mock.calls[1]?.[0]).toBe(expectedSuffix);
|
||||
expect(reply.text).toContain("📄 File: openclaw-session-session--2026-05-05T10-11-12-2.html");
|
||||
});
|
||||
|
||||
it("preserves replacement text with dollar sequences", async () => {
|
||||
const { buildExportSessionReply } = await import("./commands-export-session.js");
|
||||
hoisted.exportHtmlTemplateContents.set(
|
||||
|
||||
@@ -131,6 +131,28 @@ async function fileExists(pathName: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function addCollisionSuffix(filePath: string, suffix: number): string {
|
||||
const ext = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, ext);
|
||||
return path.join(path.dirname(filePath), `${baseName}-${suffix}${ext}`);
|
||||
}
|
||||
|
||||
async function writeNewDefaultExportFile(filePath: string, html: string): Promise<string> {
|
||||
for (let suffix = 1; suffix <= 100; suffix++) {
|
||||
const candidate = suffix === 1 ? filePath : addCollisionSuffix(filePath, suffix);
|
||||
try {
|
||||
await fsp.writeFile(candidate, html, { encoding: "utf-8", flag: "wx" });
|
||||
return candidate;
|
||||
} catch (error) {
|
||||
if (typeof error === "object" && error && "code" in error && error.code === "EEXIST") {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find an unused export filename near ${filePath}`);
|
||||
}
|
||||
|
||||
async function readSessionDataFromTranscript(sessionFile: string): Promise<{
|
||||
header: SessionHeader | null;
|
||||
entries: PiSessionEntry[];
|
||||
@@ -193,7 +215,7 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro
|
||||
// 6. Determine output path
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
const defaultFileName = `openclaw-session-${entry.sessionId.slice(0, 8)}-${timestamp}.html`;
|
||||
const outputPath = args.outputPath
|
||||
let outputPath = args.outputPath
|
||||
? path.resolve(
|
||||
args.outputPath.startsWith("~")
|
||||
? args.outputPath.replace("~", process.env.HOME ?? "")
|
||||
@@ -206,7 +228,11 @@ export async function buildExportSessionReply(params: HandleCommandsParams): Pro
|
||||
await fsp.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// 7. Write file
|
||||
await fsp.writeFile(outputPath, html, "utf-8");
|
||||
if (args.outputPath) {
|
||||
await fsp.writeFile(outputPath, html, "utf-8");
|
||||
} else {
|
||||
outputPath = await writeNewDefaultExportFile(outputPath, html);
|
||||
}
|
||||
|
||||
const relativePath = path.relative(params.workspaceDir, outputPath);
|
||||
const displayPath = relativePath.startsWith("..") ? outputPath : relativePath;
|
||||
|
||||
Reference in New Issue
Block a user