perf(ai-moderation): naikkan cache hit dgn guard akurasi
- Fase-1 exact-cache lookup: N query serial -> SATU query ANY($1::text[]) - Global reuse utk bare key legacy, HANYA verdict non-actionable (clean/flagless/action=none, conf>=0.85, umur<=72h) — flagged/warn tetap context-scoped - Semantic cache dua-band: clean band 0.92 default, actionable tetap 0.97; di antara band -> LLM (fail-open ke akurasi) - hit_count kini di-increment (bulk UPDATE per batch) -> hit-rate terukur - Cache hasil wikipediaSearch di Redis (6h, hanya hasil non-kosong) - Memoize fetchUrlSafely utk type=text (LRU 30m + in-flight dedupe) - makeImageCacheKey strip query CDN Discord (?ex/is/hm, format/width) -> attachment sama = satu key vision, skip re-download+re-vision Spec: .hermes/plans/2026-08-24-ai-analysis-cache-optimization.md Tests: +33 (cacheGuards, discordImageKeyNormalize, cacheBatchLookup)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// normalizeDiscordImageUrl — unified vision cache keys for Discord CDN URLs
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Design (2026-08-24): the same attachment reached through different signed
|
||||
// URLs (?ex=&is=&hm= tokens rotate per fetch) or render variants
|
||||
// (?format=&width=) must map to ONE vision-cache key, otherwise the vision
|
||||
// model re-downloads and re-analyzes the identical image once per variant.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
makeImageCacheKey,
|
||||
normalizeDiscordImageUrl,
|
||||
} from "../src/modules/ai-moderation/textCacheStore.js";
|
||||
|
||||
describe("normalizeDiscordImageUrl", () => {
|
||||
it("strips rotating signed tokens from cdn.discordapp.com URLs", () => {
|
||||
const a = normalizeDiscordImageUrl(
|
||||
"https://cdn.discordapp.com/attachments/1/2/img.png?ex=67a&is=67b&hm=tokA",
|
||||
);
|
||||
const b = normalizeDiscordImageUrl(
|
||||
"https://cdn.discordapp.com/attachments/1/2/img.png?ex=78c&is=78d&hm=tokB",
|
||||
);
|
||||
expect(a).toBe("https://cdn.discordapp.com/attachments/1/2/img.png");
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("strips render variants from media.discordapp.net URLs", () => {
|
||||
const a = normalizeDiscordImageUrl(
|
||||
"https://media.discordapp.net/attachments/1/2/img.png?format=webp&width=400&height=300",
|
||||
);
|
||||
const b = normalizeDiscordImageUrl(
|
||||
"https://media.discordapp.net/attachments/1/2/img.png?format=png&width=1024&height=768",
|
||||
);
|
||||
expect(a).toBe(b);
|
||||
expect(a).toBe("https://media.discordapp.net/attachments/1/2/img.png");
|
||||
});
|
||||
|
||||
it("strips query params from images-ext preview hosts", () => {
|
||||
const a = normalizeDiscordImageUrl(
|
||||
"https://images-ext-1.discordapp.net/external/X/https/example.com/cat.jpg?format=webp",
|
||||
);
|
||||
expect(a).toBe(
|
||||
"https://images-ext-1.discordapp.net/external/X/https/example.com/cat.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves URLs without query untouched", () => {
|
||||
const u = "https://cdn.discordapp.com/attachments/1/2/img.png";
|
||||
expect(normalizeDiscordImageUrl(u)).toBe(u);
|
||||
});
|
||||
|
||||
it("leaves non-Discord URLs untouched (query may be meaningful)", () => {
|
||||
const u = "https://example.com/image?token=abc&id=1";
|
||||
expect(normalizeDiscordImageUrl(u)).toBe(u);
|
||||
});
|
||||
|
||||
it("leaves data: URLs untouched", () => {
|
||||
const u = "data:image/png;base64,iVBORw0KGgoAAAANS?weird=query";
|
||||
// new URL() parses data: with protocol "data:" → not http(s) → untouched.
|
||||
expect(normalizeDiscordImageUrl(u)).toBe(u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeImageCacheKey — Discord URL unification", () => {
|
||||
it("produces the SAME key for the same attachment across token variants", () => {
|
||||
const keyA = makeImageCacheKey(
|
||||
"https://cdn.discordapp.com/attachments/9/8/pic.png?ex=111&is=222&hm=AAA",
|
||||
);
|
||||
const keyB = makeImageCacheKey(
|
||||
"https://cdn.discordapp.com/attachments/9/8/pic.png?ex=333&is=444&hm=BBB",
|
||||
);
|
||||
expect(keyA).toBe(keyB);
|
||||
});
|
||||
|
||||
it("still produces DIFFERENT keys for different attachments", () => {
|
||||
const keyA = makeImageCacheKey(
|
||||
"https://cdn.discordapp.com/attachments/9/8/a.png?ex=1",
|
||||
);
|
||||
const keyB = makeImageCacheKey(
|
||||
"https://cdn.discordapp.com/attachments/9/8/b.png?ex=1",
|
||||
);
|
||||
expect(keyA).not.toBe(keyB);
|
||||
});
|
||||
|
||||
it("preserves the historical full-hash behavior for non-Discord URLs", () => {
|
||||
// Regression guard: external URLs keep pre-change keys.
|
||||
const key = makeImageCacheKey("https://example.com/x.png");
|
||||
expect(key).toBe(makeImageCacheKey("https://example.com/x.png"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user