diff --git a/services/discord-gateway/tests/imageCacheKey.test.ts b/services/discord-gateway/tests/imageCacheKey.test.ts new file mode 100644 index 0000000..9032571 --- /dev/null +++ b/services/discord-gateway/tests/imageCacheKey.test.ts @@ -0,0 +1,51 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// makeImageCacheKey — regression for hash collision bug +// ═══════════════════════════════════════════════════════════════════════════ +// Bug (2026-08-12): makeImageCacheKey() only hashed the first 128 chars of the +// data URL. Since all resized images share the same MIME prefix +// ('data:image/png;base64,') + identical base64 header bytes, nearly every +// image got the SAME hash → 'image:' → all images reused the +// first cached vision analysis ("konten judi"). +// +// Fix: hash the ENTIRE data URL. This test verifies the fix and prevents +// regression. +import { createHash } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { makeImageCacheKey } from "../src/modules/ai-moderation/textCacheStore.js"; + +function oldBuggyHash(dataUrl: string): string { + const prefix = dataUrl.slice(0, 128); + return "image:" + createHash("sha256").update(prefix).digest("hex").slice(0, 16); +} + +describe("makeImageCacheKey — collision prevention", () => { + it("produces different keys for images whose first 128 chars are identical", () => { + // Two data URLs that SHARE the first 128 chars (same MIME + identical + // base64 header) but differ after — this is the real-world scenario + // that caused the collision bug. + const sharedPrefix = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // pad to >128 chars + + const imgA = sharedPrefix + "UNIQUE_TO_A"; + const imgB = sharedPrefix + "UNIQUE_TO_B"; + + // Under the OLD buggy scheme: same prefix → same hash → COLLISION + expect(oldBuggyHash(imgA)).toBe(oldBuggyHash(imgB)); + + // Under the FIXED scheme: full data URL hashed → different keys + const keyA = makeImageCacheKey(imgA); + const keyB = makeImageCacheKey(imgB); + expect(keyA).not.toBe(keyB); + }); + + it("produces same key for identical input", () => { + const dataUrl = "data:image/png;base64,samebase64dataheremari"; + expect(makeImageCacheKey(dataUrl)).toBe(makeImageCacheKey(dataUrl)); + }); + + it("prefix is always 'image:'", () => { + const key = makeImageCacheKey("data:image/png;base64,test"); + expect(key.startsWith("image:")).toBe(true); + }); +});