fix(qqbot): use truncateUtf16Safe for debug log URL/key truncation (#102572)

* fix(qqbot): use truncateUtf16Safe for debug log URL/key truncation

Replace naive .slice(0, N) with truncateUtf16Safe() in:
- image-size.ts: URL preview in debug logs (2 sites)
- upload-cache.ts: cache key preview in debug logs (2 sites)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(qqbot): cover UTF-16-safe debug previews

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
lsr911
2026-07-09 18:20:38 +08:00
committed by GitHub
parent 06b33f5462
commit 1beea4b873
4 changed files with 64 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const adapterMocks = vi.hoisted(() => ({
fetchMedia: vi.fn(),
debugLog: vi.fn(),
}));
vi.mock("../adapter/index.js", () => ({
@@ -12,6 +13,10 @@ vi.mock("../adapter/index.js", () => ({
}),
}));
vi.mock("./log.js", () => ({
debugLog: (...args: unknown[]) => adapterMocks.debugLog(...args),
}));
import { getImageSizeFromUrl, parseImageSize } from "./image-size.js";
/** Build a minimal valid PNG header with the given dimensions. */
@@ -39,6 +44,7 @@ function buildPngHeader(width: number, height: number): Buffer {
describe("getImageSizeFromUrl", () => {
beforeEach(() => {
adapterMocks.fetchMedia.mockReset();
adapterMocks.debugLog.mockReset();
});
describe("fetchMedia options contract", () => {
@@ -140,6 +146,27 @@ describe("getImageSizeFromUrl", () => {
expect(size).toBeNull();
});
it("logs fetched URLs without splitting surrogate pairs", async () => {
adapterMocks.fetchMedia.mockResolvedValueOnce({
buffer: buildPngHeader(800, 600),
contentType: "image/png",
});
const base = "https://cdn.example.com/";
const urlPrefix = `${base}${"x".repeat(59 - base.length)}`;
await getImageSizeFromUrl(`${urlPrefix}😀.png`);
expect(adapterMocks.debugLog).toHaveBeenCalledWith(
`[image-size] Got size from URL: 800x600 - ${urlPrefix}...`,
);
adapterMocks.fetchMedia.mockRejectedValueOnce(new Error("probe failed"));
await getImageSizeFromUrl(`${urlPrefix}😀.png`);
expect(adapterMocks.debugLog).toHaveBeenLastCalledWith(
`[image-size] Error fetching ${urlPrefix}...: probe failed`,
);
});
});
});

View File

@@ -5,6 +5,7 @@
*/
import { Buffer } from "node:buffer";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { getPlatformAdapter } from "../adapter/index.js";
import type { SsrfPolicyConfig } from "../adapter/types.js";
import { formatErrorMessage } from "./format.js";
@@ -185,7 +186,7 @@ export async function getImageSizeFromUrl(
const size = parseImageSize(buffer);
if (size) {
debugLog(
`[image-size] Got size from URL: ${size.width}x${size.height} - ${url.slice(0, 60)}...`,
`[image-size] Got size from URL: ${size.width}x${size.height} - ${truncateUtf16Safe(url, 60)}...`,
);
}
return size;
@@ -193,7 +194,9 @@ export async function getImageSizeFromUrl(
clearTimeout(timeoutId);
}
} catch (err) {
debugLog(`[image-size] Error fetching ${url.slice(0, 60)}...: ${formatErrorMessage(err)}`);
debugLog(
`[image-size] Error fetching ${truncateUtf16Safe(url, 60)}...: ${formatErrorMessage(err)}`,
);
return null;
}
}

View File

@@ -1,11 +1,21 @@
// Qqbot tests cover upload cache plugin behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
debugLog: vi.fn(),
}));
vi.mock("./log.js", () => ({
debugLog: (...args: unknown[]) => mocks.debugLog(...args),
}));
import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./upload-cache.js";
describe("qqbot upload-cache", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
mocks.debugLog.mockReset();
});
it("reuses cached file info before expiry", () => {
@@ -32,4 +42,21 @@ describe("qqbot upload-cache", () => {
expect(getCachedFileInfo(hash, "group", "target-overflow", 1)).toBeNull();
});
it("logs cache keys without splitting surrogate pairs", () => {
const hash = computeFileHash("qqbot-surrogate-key");
const keyPrefix = `${hash}:group:`;
setCachedFileInfo(hash, "group", "😀target", 1, "file-info", "uuid-safe", 3600);
expect(getCachedFileInfo(hash, "group", "😀target", 1)).toBe("file-info");
expect(mocks.debugLog).toHaveBeenNthCalledWith(
1,
`[upload-cache] Cache SET: key=${keyPrefix}..., ttl=3540s, uuid=uuid-safe`,
);
expect(mocks.debugLog).toHaveBeenNthCalledWith(
2,
`[upload-cache] Cache HIT: key=${keyPrefix}..., fileUuid=uuid-safe`,
);
});
});

View File

@@ -8,6 +8,7 @@ import {
isFutureDateTimestampMs,
resolveExpiresAtMsFromDurationSeconds,
} from "openclaw/plugin-sdk/number-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { ChatScope } from "../types.js";
import { debugLog } from "./log.js";
@@ -55,7 +56,9 @@ export function getCachedFileInfo(
return null;
}
debugLog(`[upload-cache] Cache HIT: key=${key.slice(0, 40)}..., fileUuid=${entry.fileUuid}`);
debugLog(
`[upload-cache] Cache HIT: key=${truncateUtf16Safe(key, 40)}..., fileUuid=${entry.fileUuid}`,
);
return entry.fileInfo;
}
@@ -100,6 +103,6 @@ export function setCachedFileInfo(
});
debugLog(
`[upload-cache] Cache SET: key=${key.slice(0, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`,
`[upload-cache] Cache SET: key=${truncateUtf16Safe(key, 40)}..., ttl=${effectiveTtl}s, uuid=${fileUuid}`,
);
}