fix(qqbot): avoid log export filename collisions (#77765)

* fix(qqbot): avoid log export filename collisions

* test(qqbot): narrow log export result assertions
This commit is contained in:
Vincent Koc
2026-05-05 02:11:33 -07:00
committed by GitHub
parent 6caa365a7a
commit a732208d45
2 changed files with 87 additions and 2 deletions

View File

@@ -0,0 +1,61 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const platformMock = await vi.hoisted(async () => {
const fs = await import("node:fs");
const path = await import("node:path");
return {
fs,
homeDir: "",
path,
};
});
vi.mock("../../utils/platform.js", () => ({
getHomeDir: () => platformMock.homeDir,
getQQBotDataDir: (...subPaths: string[]) => {
const dir = platformMock.path.join(platformMock.homeDir, ".openclaw", "qqbot", ...subPaths);
platformMock.fs.mkdirSync(dir, { recursive: true });
return dir;
},
isWindows: () => false,
}));
import { buildBotLogsResult } from "./log-helpers.js";
describe("buildBotLogsResult", () => {
let tempHome: string;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-qqbot-logs-"));
platformMock.homeDir = tempHome;
});
afterEach(() => {
vi.useRealTimers();
fs.rmSync(tempHome, { recursive: true, force: true });
});
it("suffixes same-second log exports instead of overwriting", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-05T10:11:12.345Z"));
const logDir = path.join(tempHome, ".openclaw", "logs");
fs.mkdirSync(logDir, { recursive: true });
fs.writeFileSync(path.join(logDir, "gateway.log"), "line 1\nline 2\n", "utf8");
const first = buildBotLogsResult();
const second = buildBotLogsResult();
expect(typeof first).toBe("object");
expect(typeof second).toBe("object");
if (!first || !second || typeof first === "string" || typeof second === "string") {
throw new Error("expected file upload results");
}
expect(path.basename(first.filePath)).toBe("bot-logs-2026-05-05T10-11-12.txt");
expect(path.basename(second.filePath)).toBe("bot-logs-2026-05-05T10-11-12-2.txt");
expect(fs.readFileSync(first.filePath, "utf8")).toContain("line 1");
expect(fs.readFileSync(second.filePath, "utf8")).toContain("line 2");
});
});

View File

@@ -128,6 +128,28 @@ type LogCandidate = {
mtimeMs: number;
};
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}`);
}
function writeNewTextFileSync(filePath: string, contents: string): string {
for (let suffix = 1; suffix <= 100; suffix++) {
const candidate = suffix === 1 ? filePath : addCollisionSuffix(filePath, suffix);
try {
fs.writeFileSync(candidate, contents, { encoding: "utf8", 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 log export filename near ${filePath}`);
}
function collectRecentLogFiles(logDirs: string[]): LogCandidate[] {
const candidates: LogCandidate[] = [];
const dedupe = new Set<string>();
@@ -303,8 +325,10 @@ export function buildBotLogsResult(): SlashCommandResult {
const tmpDir = getQQBotDataDir("downloads");
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const tmpFile = path.join(tmpDir, `bot-logs-${timestamp}.txt`);
fs.writeFileSync(tmpFile, lines.join("\n"), "utf8");
const tmpFile = writeNewTextFileSync(
path.join(tmpDir, `bot-logs-${timestamp}.txt`),
lines.join("\n"),
);
const fileCount = recentFiles.length;
const topSources = Array.from(new Set(recentFiles.map((item) => item.sourceDir))).slice(0, 3);