From 3f89aae98da9229ea7cf9bfc6215aaeae01a8796 Mon Sep 17 00:00:00 2001 From: RileyJJY <0668000974@xydigit.com> Date: Fri, 17 Jul 2026 03:12:46 +0800 Subject: [PATCH] fix(qqbot): bound tail log reads to actual bytes returned by fs.readSync (#108955) * fix(qqbot): bound tail log reads to actual bytes returned by fs.readSync * fix(qqbot): satisfy knip deadcode check for testing export Add __testing re-export and test-api.ts barrel so knip traces the testing export through a recognized entry point. * fix(qqbot): restore testing export alongside __testing re-export Both exports are needed: testing for proof scripts, __testing for knip tracing. * fix(qqbot): remove unused __testing re-export from log-helpers test-api.ts already imports testing and re-exports as __testing. The extra re-export in log-helpers.ts was unused by production code. * fix(qqbot): retry short log tail reads * test(qqbot): keep short-read seam private Co-authored-by: RileyJJY <100176083+RileyJJY@users.noreply.github.com> * test(qqbot): exercise short reads through log export Co-authored-by: RileyJJY <0668000974@xydigit.com> --------- Co-authored-by: Peter Steinberger Co-authored-by: RileyJJY <100176083+RileyJJY@users.noreply.github.com> --- .../commands/builtin/log-helpers.test.ts | 36 +++++++++++++++++++ .../engine/commands/builtin/log-helpers.ts | 22 +++++++++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/extensions/qqbot/src/engine/commands/builtin/log-helpers.test.ts b/extensions/qqbot/src/engine/commands/builtin/log-helpers.test.ts index 20a04624d71b..5133065d0621 100644 --- a/extensions/qqbot/src/engine/commands/builtin/log-helpers.test.ts +++ b/extensions/qqbot/src/engine/commands/builtin/log-helpers.test.ts @@ -35,6 +35,7 @@ describe("buildBotLogsResult", () => { }); afterEach(() => { + vi.restoreAllMocks(); vi.useRealTimers(); fs.rmSync(tempHome, { recursive: true, force: true }); }); @@ -59,4 +60,39 @@ describe("buildBotLogsResult", () => { expect(fs.readFileSync(first.filePath, "utf8")).toContain("line 1"); expect(fs.readFileSync(second.filePath, "utf8")).toContain("line 2"); }); + + it("completes short fs.readSync tail windows before selecting lines", () => { + const logDir = path.join(tempHome, ".openclaw", "logs"); + fs.mkdirSync(logDir, { recursive: true }); + const logFile = path.join(logDir, "gateway.log"); + const lines = Array.from( + { length: 40 }, + (_, index) => `line ${String(index + 1).padStart(2, "0")}`, + ); + const contents = `${lines.join("\n")}\n`; + fs.writeFileSync(logFile, contents, "utf8"); + + const realReadSync = fs.readSync.bind(fs) as typeof fs.readSync; + const readSpy = vi.spyOn(fs, "readSync").mockImplementation((( + fd: number, + buffer: NodeJS.ArrayBufferView, + offset: number, + length: number, + position: number | null, + ) => { + return realReadSync(fd, buffer, offset, Math.min(length, 7), position); + }) as typeof fs.readSync); + + const result = buildBotLogsResult(); + + expect(readSpy.mock.calls.length).toBeGreaterThan(1); + expect(typeof result).toBe("object"); + if (!result || typeof result === "string") { + throw new Error("expected file upload result"); + } + const exportedLogs = fs.readFileSync(result.filePath, "utf8"); + expect(exportedLogs).toContain("line 01"); + expect(exportedLogs).toContain("line 40"); + expect(exportedLogs).not.toContain("\0"); + }); }); diff --git a/extensions/qqbot/src/engine/commands/builtin/log-helpers.ts b/extensions/qqbot/src/engine/commands/builtin/log-helpers.ts index c59f2159d731..17db71b1e698 100644 --- a/extensions/qqbot/src/engine/commands/builtin/log-helpers.ts +++ b/extensions/qqbot/src/engine/commands/builtin/log-helpers.ts @@ -230,11 +230,25 @@ function tailFileLines( const readSize = Math.min(CHUNK_SIZE, position); position -= readSize; const buf = Buffer.alloc(readSize); - fs.readSync(fd, buf, 0, readSize, position); - chunks.unshift(buf); - bytesRead += readSize; + let actualRead = 0; + while (actualRead < readSize) { + const justRead = fs.readSync( + fd, + buf, + actualRead, + readSize - actualRead, + position + actualRead, + ); + if (justRead === 0) { + throw new Error(`Could not complete log read for ${filePath}`); + } + actualRead += justRead; + } - for (let i = 0; i < readSize; i++) { + chunks.unshift(buf); + bytesRead += actualRead; + + for (let i = 0; i < actualRead; i++) { if (buf[i] === 0x0a) { newlineCount++; }