Files
GMW/services/discord-gateway/tests/cacheBatchLookup.test.ts
T
mytheclipsebotreviewandgit-migration[bot] 7be069d73f chore: migrate monorepo toolchain pnpm+vitest → bun (bun test, bunfig preload, bun.lock)
- Gateway + backend + frontend: pnpm/vitest fully removed → bun 1.3.14
  (bun install, bun test tests/, bunfig.toml [test] preload, bun.lock).
- vitest configs deleted; vitest→bun facade (jest/mock/spyOn/waitForCompat)
  keeps the vitest-style assertions working under bun:test.
- flake.nix: bunInstall switch; pruned prod-pass now removes post-pnpm
  dev-toolchain trees; frontend builds Next standalone via bun's next.
- CI: deploy.yml installs with bun + runs bun test tests/ per service.
- 190 tests green (146 gateway + 37 backend pass, 14 skip), 0 fail;
  tsc + biome across all 3 services clean.

Co-authored-by: git-migration[bot] <noreply@gmw.local>
2026-09-24 23:50:30 +07:00

128 lines
4.4 KiB
TypeScript

// ═══════════════════════════════════════════════════════════════════════════
// Batched exact-cache lookup (getCachedTextModerations)
// ═══════════════════════════════════════════════════════════════════════════
// Design (2026-08-24): phase-1 cache lookups collapse N sequential per-key
// queries into ONE `text = ANY($1::text[])` query. Semantics must match the
// single-key getter: unexpired rows only, malformed rows skipped, verdicts
// normalized through the shared parser. The DB layer is mocked — no live
// Postgres in unit tests.
import { beforeEach, describe, expect, it, jest, mock } from "bun:test";
const executeAll = jest.fn();
const executeGet = jest.fn();
mock.module("../src/shared/database/drizzle.js", () => ({
executeAll: (...args: unknown[]) => executeAll(...args),
executeGet: (...args: unknown[]) => executeGet(...args),
}));
import {
getCachedTextModerations,
parseStoredVerdictRow,
} from "../src/modules/ai-moderation/textCacheStore.js";
function rowFor(
text: string,
status: string,
analyzedAt = Date.now() - 1000,
): Record<string, unknown> {
return {
text,
flags: JSON.stringify({
status,
flags: [],
score: 0,
analysis: "ok",
categories: [],
severity: "none",
confidence: 0.9,
recommendedAction: "none",
}),
source: "user_moderation",
analyzed_at: analyzedAt,
expires_at: Date.now() + 3_600_000,
hit_count: 0,
};
}
beforeEach(() => {
executeAll.mockReset();
executeGet.mockReset();
});
describe("parseStoredVerdictRow", () => {
it("parses a valid stored verdict", () => {
const v = parseStoredVerdictRow({
flags: JSON.stringify({ status: "warn", flags: ["x"] }),
});
expect(v).not.toBeNull();
expect(v?.status).toBe("warn");
expect(v?.flags).toEqual(["x"]);
});
it("returns null on malformed JSON", () => {
expect(parseStoredVerdictRow({ flags: "{not-json" })).toBeNull();
});
it("derives legacy status from flags when status is absent", () => {
const v = parseStoredVerdictRow({
flags: JSON.stringify({ flags: ["a"] }),
});
expect(v?.status).toBe("flagged");
const v2 = parseStoredVerdictRow({ flags: JSON.stringify({}) });
expect(v2?.status).toBe("clean");
});
});
describe("getCachedTextModerations", () => {
it("returns an empty map for empty input and issues no query", async () => {
const result = await getCachedTextModerations([]);
expect(result.size).toBe(0);
expect(executeAll).not.toHaveBeenCalled();
});
it("dedupes keys and returns parsed verdicts keyed by cache key", async () => {
executeAll.mockResolvedValueOnce([
rowFor("text_mod:c1:aaa", "clean"),
rowFor("text_mod:c1:bbb", "warn"),
]);
const result = await getCachedTextModerations([
"text_mod:c1:aaa",
"text_mod:c1:aaa",
"text_mod:c1:bbb",
]);
expect(result.size).toBe(2);
expect(result.get("text_mod:c1:aaa")?.verdict.status).toBe("clean");
expect(result.get("text_mod:c1:bbb")?.verdict.status).toBe("warn");
// Exactly ONE batched round-trip.
expect(executeAll).toHaveBeenCalledTimes(1);
});
it("skips rows with malformed payloads instead of failing the batch", async () => {
executeAll.mockResolvedValueOnce([
{
text: "k_good",
flags: JSON.stringify({ status: "clean", flags: [] }),
analyzed_at: 1,
},
{ text: "k_bad", flags: "{broken" },
]);
const result = await getCachedTextModerations(["k_good", "k_bad"]);
expect(result.has("k_good")).toBe(true);
expect(result.has("k_bad")).toBe(false);
});
it("survives a DB failure and returns an empty map (fail-open)", async () => {
executeAll.mockRejectedValueOnce(new Error("connection refused"));
const result = await getCachedTextModerations(["k1", "k2"]);
expect(result.size).toBe(0);
});
it("chunks queries beyond 200 keys", async () => {
executeAll.mockResolvedValue([]);
const keys = Array.from({ length: 450 }, (_, i) => `k${i}`);
await getCachedTextModerations(keys);
expect(executeAll).toHaveBeenCalledTimes(3); // 200 + 200 + 50
});
});