fix(ai): pertahankan status warn di cache moderasi + bersihkan prompt stale

- normalizeStoredStatus(): exact-hash & semantic (Qdrant/PG) cache reader
  sebelumnya menipiskan 'warn' jadi 'flagged'/'clean' (type narrowing
  legacy clean|flagged) — merusak gating auto-delete & label dashboard.
  Kini status tersimpan dipertahankan penuh (clean/warn/flagged).
- prompts: hapus referensi <user_history> yang tak pernah di-inject,
  SearXNG -> Wikipedia (sudah migrasi), referensi section yang tak ada,
  typo 'secifik', dan baris list rusak '|-'.
- moderationBuilders: buang dead code buildUserProfilesBlock/
  buildUserProfileRef/UserProfileEntry/buildUserHistoryXml (tanpa caller
  produksi sejak context minimization) + test-nya.
- test baru: tests/storedStatusNormalization.test.ts (regresi warn).
This commit is contained in:
asepharyana
2026-08-22 16:17:55 +07:00
parent 4ffc99b3fe
commit 1397380fe9
7 changed files with 115 additions and 217 deletions
@@ -1,11 +1,10 @@
// ═══════════════════════════════════════════════════════════════════════════
// Context enrichment builders — <user_history>, <user_profiles> as_of,
// bot/edited detection (pure, no DB)
// Context enrichment builders — bot/edited detection (pure, no DB)
// (buildUserHistoryXml / buildUserProfilesBlock were removed with the
// per-user context minimization; their tests went with them.)
// ═══════════════════════════════════════════════════════════════════════════
import { describe, expect, it } from "vitest";
import {
buildUserHistoryXml,
buildUserProfilesBlock,
resolveIsBot,
resolveIsEdited,
} from "../src/modules/ai-moderation/moderationBuilders.js";
@@ -39,85 +38,6 @@ function msg(overrides: Partial<MessageRecord> = {}): MessageRecord {
};
}
const DAY_MS = 24 * 60 * 60 * 1000;
describe("buildUserHistoryXml — last flagged messages for repeat offenders", () => {
it("returns empty when there is no real history", () => {
expect(buildUserHistoryXml([])).toBe("");
expect(
buildUserHistoryXml([{ content: " ", severity: "low", created_at: 1 }]),
).toBe("");
});
it("renders <infraction> rows with severity and recency", () => {
const xml = buildUserHistoryXml(
[
{
content: "beli barang murah disini https://scam.example",
severity: "high",
created_at: NOW - 3 * DAY_MS,
},
],
NOW,
);
expect(xml).toContain("<user_history>");
expect(xml).toContain('severity="high"');
expect(xml).toContain('time_ago_days="3"');
expect(xml).toContain("beli barang murah disini");
});
it("caps long snippets and XML-escapes content", () => {
const xml = buildUserHistoryXml(
[
{
content: "x".repeat(300),
severity: "low",
created_at: NOW - DAY_MS,
},
],
NOW,
);
expect(xml.length).toBeLessThan(250);
});
});
describe("buildUserProfilesBlock — deduplicated map with staleness", () => {
it("emits as_of when the profile has a last-generated timestamp", () => {
const block = buildUserProfilesBlock(
new Map([
[
"u1",
{
text: "Developer teknis, bahasa Indonesia",
asOf: NOW - 3 * DAY_MS,
},
],
]),
);
expect(block).toContain('<user_profile user_id="u1"');
expect(block).toContain(
`as_of="${new Date(NOW - 3 * DAY_MS).toISOString()}"`,
);
expect(block).toContain("Developer teknis");
});
it("omits as_of when absent, and drops empty profiles", () => {
const block = buildUserProfilesBlock(
new Map([
["u1", { text: "profil aktif", asOf: null }],
["u2", { text: " " }],
]),
);
expect(block).toContain('user_id="u1"');
expect(block).not.toContain("as_of");
expect(block).not.toContain("u2");
});
it("returns empty for no profiles", () => {
expect(buildUserProfilesBlock(new Map())).toBe("");
});
});
describe("resolveIsBot / resolveIsEdited — message flags", () => {
it("reads author.bot from captured metadata", () => {
const bot = msg({
@@ -0,0 +1,37 @@
// ═══════════════════════════════════════════════════════════════════════════
// Stored-status normalization — "warn" verdicts must survive the cache
// ═══════════════════════════════════════════════════════════════════════════
// Bug (2026-08-22): getCachedTextModeration() narrowed its return type to
// "clean" | "flagged". A stored "warn" verdict with flags (e.g.
// ["conflict_instigation"]) fell into the legacy `flags.length === 0 ?
// clean : flagged` branch and was read back as FLAGGED. Downstream this
// broke auto-delete eligibility gating and mislabelled warnings on the
// dashboard. parseQdrantVerdict had the same narrowing (warn → clean).
//
// Fix: normalizeStoredStatus() accepts the full clean/warn/flagged union in
// BOTH readers; unknown/legacy values still derive from flags.
import { describe, expect, it } from "vitest";
import { normalizeStoredStatus } from "../src/modules/ai-moderation/textCacheStore.js";
describe("normalizeStoredStatus — warn survives cache round-trip", () => {
it("keeps a stored 'warn' status as 'warn'", () => {
expect(normalizeStoredStatus("warn", ["conflict_instigation"])).toBe(
"warn",
);
});
it("keeps stored 'clean' and 'flagged' unchanged", () => {
expect(normalizeStoredStatus("clean", [])).toBe("clean");
expect(normalizeStoredStatus("flagged", ["sara"])).toBe("flagged");
});
it("derives from flags for legacy entries without a stored status", () => {
expect(normalizeStoredStatus(undefined, [])).toBe("clean");
expect(normalizeStoredStatus(undefined, ["spam"])).toBe("flagged");
});
it("treats an unknown stored status like a legacy entry", () => {
expect(normalizeStoredStatus("processing", [])).toBe("clean");
expect(normalizeStoredStatus("processing", ["spam"])).toBe("flagged");
});
});