From bfceffa2f769a9ec8e61662b96e6adeecb3474b0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 12:46:56 -0400 Subject: [PATCH] fix(qqbot): bound upload cache expiry --- .../src/engine/utils/upload-cache.test.ts | 34 +++++++++++++++++++ .../qqbot/src/engine/utils/upload-cache.ts | 15 ++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 extensions/qqbot/src/engine/utils/upload-cache.test.ts diff --git a/extensions/qqbot/src/engine/utils/upload-cache.test.ts b/extensions/qqbot/src/engine/utils/upload-cache.test.ts new file mode 100644 index 00000000000..04ddcc85d58 --- /dev/null +++ b/extensions/qqbot/src/engine/utils/upload-cache.test.ts @@ -0,0 +1,34 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./upload-cache.js"; + +describe("qqbot upload-cache", () => { + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("reuses cached file info before expiry", () => { + const hash = computeFileHash("qqbot-cache-hit"); + + setCachedFileInfo(hash, "group", "target-hit", 1, "file-info-hit", "uuid-hit", 3600); + + expect(getCachedFileInfo(hash, "group", "target-hit", 1)).toBe("file-info-hit"); + }); + + it("drops cached file info when the current clock is invalid", () => { + const hash = computeFileHash("qqbot-invalid-clock"); + setCachedFileInfo(hash, "group", "target-invalid-clock", 1, "file-info-invalid", "uuid", 3600); + vi.spyOn(Date, "now").mockReturnValue(Number.NaN); + + expect(getCachedFileInfo(hash, "group", "target-invalid-clock", 1)).toBeNull(); + }); + + it("does not cache file info when ttl expiry exceeds the Date range", () => { + vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000); + const hash = computeFileHash("qqbot-overflow"); + + setCachedFileInfo(hash, "group", "target-overflow", 1, "file-info-overflow", "uuid", 3600); + + expect(getCachedFileInfo(hash, "group", "target-overflow", 1)).toBeNull(); + }); +}); diff --git a/extensions/qqbot/src/engine/utils/upload-cache.ts b/extensions/qqbot/src/engine/utils/upload-cache.ts index 2ca8e7a6b6e..c00a326a4de 100644 --- a/extensions/qqbot/src/engine/utils/upload-cache.ts +++ b/extensions/qqbot/src/engine/utils/upload-cache.ts @@ -4,6 +4,10 @@ */ import * as crypto from "node:crypto"; +import { + isFutureDateTimestampMs, + resolveExpiresAtMsFromDurationSeconds, +} from "openclaw/plugin-sdk/number-runtime"; import type { ChatScope } from "../types.js"; import { debugLog } from "./log.js"; @@ -46,7 +50,7 @@ export function getCachedFileInfo( return null; } - if (Date.now() >= entry.expiresAt) { + if (!isFutureDateTimestampMs(entry.expiresAt)) { cache.delete(key); return null; } @@ -68,7 +72,7 @@ export function setCachedFileInfo( if (cache.size >= MAX_CACHE_SIZE) { const now = Date.now(); for (const [k, v] of cache) { - if (now >= v.expiresAt) { + if (!isFutureDateTimestampMs(v.expiresAt, { nowMs: now })) { cache.delete(k); } } @@ -83,11 +87,16 @@ export function setCachedFileInfo( const key = buildCacheKey(contentHash, scope, targetId, fileType); const safetyMargin = 60; const effectiveTtl = Math.max(ttl - safetyMargin, 10); + const expiresAt = resolveExpiresAtMsFromDurationSeconds(effectiveTtl); + if (expiresAt === undefined) { + cache.delete(key); + return; + } cache.set(key, { fileInfo, fileUuid, - expiresAt: Date.now() + effectiveTtl * 1000, + expiresAt, }); debugLog(