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 <steipete@gmail.com>
Co-authored-by: RileyJJY <100176083+RileyJJY@users.noreply.github.com>
This commit is contained in:
RileyJJY
2026-07-17 03:12:46 +08:00
committed by GitHub
parent 63b0cac9f0
commit 3f89aae98d
2 changed files with 54 additions and 4 deletions

View File

@@ -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");
});
});

View File

@@ -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++;
}