- mediaDownloader: flip URL candidate order so discord_url is tried before uploaded_url (uploaded_url is archive-only fallback) - ai-analysis-worker: remove upload-pending race guard that blocked analysis until Tele upload completed; analysis now runs immediately on the Discord CDN URL - batchProcessor: remove upload-pending defer/poll-backoff logic - individualFallbackProcessor: remove upload_pending requeue loop - batchOutcomeClassifier/fallbackResultClassifier: drop upload_pending classification (no longer needed) - tests: update batchOutcomeClassifier + fallbackResultClassifier tests to reflect removed upload_pending signal
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
// ═══════════════════════════════════════════════════════════════════════════
|
|
// partitionBatchOutcome — per-message disposition (2026-08-28)
|
|
//
|
|
// Upload-pending race guard removed: analysis no longer depends on the Tele
|
|
// uploader. The worker always runs analysis on the Discord CDN URL directly,
|
|
// so there are no upload-pending targets to defer.
|
|
import { describe, expect, it } from "vitest";
|
|
import { partitionBatchOutcome } from "../src/modules/ai-moderation/batchOutcomeClassifier.js";
|
|
|
|
const msgs = (...ids: string[]) => ids.map((id) => ({ id }));
|
|
|
|
describe("partitionBatchOutcome", () => {
|
|
it("classifies a fully-analyzed batch as completed", () => {
|
|
const out = partitionBatchOutcome(msgs("a", "b"), {
|
|
ok: true,
|
|
rows: [
|
|
{ id: "a", ai_status: "clean" },
|
|
{ id: "b", ai_status: "flagged" },
|
|
],
|
|
});
|
|
expect(out.get("a")).toBe("completed");
|
|
expect(out.get("b")).toBe("completed");
|
|
});
|
|
|
|
it("treats an unexplained missing row as incomplete (retryable)", () => {
|
|
const out = partitionBatchOutcome(msgs("a", "gone1"), {
|
|
ok: true,
|
|
rows: [{ id: "a", ai_status: "clean" }],
|
|
// gone1 is missing → incomplete
|
|
});
|
|
expect(out.get("a")).toBe("completed");
|
|
expect(out.get("gone1")).toBe("incomplete");
|
|
});
|
|
|
|
it("routes flag-based failures to their buckets", () => {
|
|
const out = partitionBatchOutcome(msgs("i", "p", "f"), {
|
|
ok: true,
|
|
rows: [
|
|
{
|
|
id: "i",
|
|
ai_status: "error",
|
|
ai_moderation_flags: JSON.stringify(["analysis_incomplete"]),
|
|
},
|
|
{
|
|
id: "p",
|
|
ai_status: "error",
|
|
ai_moderation_flags: JSON.stringify(["analysis_parse_failed"]),
|
|
},
|
|
{
|
|
id: "f",
|
|
ai_status: "error",
|
|
ai_moderation_flags: JSON.stringify(["analysis_api_failed"]),
|
|
},
|
|
],
|
|
});
|
|
expect(out.get("i")).toBe("incomplete");
|
|
expect(out.get("p")).toBe("parse_failed");
|
|
expect(out.get("f")).toBe("api_failed");
|
|
});
|
|
|
|
it("treats error rows with no known flag as retryable incomplete", () => {
|
|
const out = partitionBatchOutcome(msgs("x"), {
|
|
ok: true,
|
|
rows: [
|
|
{ id: "x", ai_status: "error", ai_moderation_flags: '["weird_flag"]' },
|
|
],
|
|
});
|
|
expect(out.get("x")).toBe("incomplete");
|
|
});
|
|
|
|
it("accepts DB-round-trip MessageRecord shape (null ai_status)", () => {
|
|
const out = partitionBatchOutcome([{ id: "r", ai_status: null }] as never, {
|
|
ok: true,
|
|
rows: [{ id: "r", ai_status: null }],
|
|
});
|
|
expect(out.get("r")).toBe("completed");
|
|
});
|
|
});
|