feat(discord-gateway): use Discord CDN for image analysis, uploader archive-only
- 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
This commit is contained in:
@@ -1,53 +1,35 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// partitionBatchOutcome — upload-pending defer vs fanout (2026-08-25)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Bug history: the batch worker's race guard returned {ok:true, rows:[]} while
|
||||
// attachments were still uploading; every target was classified "incomplete",
|
||||
// fanned out to the individual queue, requeued there, rescheduled at 250ms —
|
||||
// a hot ~300ms loop for the whole upload duration (~10 cycles in 3s in prod).
|
||||
// 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 {
|
||||
computeUploadPollDelayMs,
|
||||
partitionBatchOutcome,
|
||||
} from "../src/modules/ai-moderation/batchOutcomeClassifier.js";
|
||||
import { partitionBatchOutcome } from "../src/modules/ai-moderation/batchOutcomeClassifier.js";
|
||||
|
||||
const msgs = (...ids: string[]) => ids.map((id) => ({ id }));
|
||||
|
||||
describe("partitionBatchOutcome", () => {
|
||||
it("marks ALL targets upload_pending when the full-batch guard fires", () => {
|
||||
it("classifies a fully-analyzed batch as completed", () => {
|
||||
const out = partitionBatchOutcome(msgs("a", "b"), {
|
||||
ok: true,
|
||||
rows: [],
|
||||
uploadPendingIds: ["a", "b"],
|
||||
});
|
||||
expect(out.get("a")).toBe("upload_pending");
|
||||
expect(out.get("b")).toBe("upload_pending");
|
||||
});
|
||||
|
||||
it("never classifies an explicit upload_pending id as incomplete", () => {
|
||||
// The regression this file exists for: uploadPendingIds must win over the
|
||||
// missing-row heuristic.
|
||||
const out = partitionBatchOutcome(msgs("a"), {
|
||||
ok: true,
|
||||
rows: [],
|
||||
uploadPendingIds: ["a"],
|
||||
});
|
||||
expect(out.get("a")).not.toBe("incomplete");
|
||||
expect(out.get("a")).toBe("upload_pending");
|
||||
});
|
||||
|
||||
it("partitions a mixed batch: completed + upload-pending + missing", () => {
|
||||
const out = partitionBatchOutcome(msgs("ok1", "up1", "gone1"), {
|
||||
ok: true,
|
||||
rows: [
|
||||
{ id: "ok1", ai_status: "clean" },
|
||||
// up1 has NO row but IS in uploadPendingIds -> deferred, not failed
|
||||
{ id: "a", ai_status: "clean" },
|
||||
{ id: "b", ai_status: "flagged" },
|
||||
],
|
||||
uploadPendingIds: ["up1"],
|
||||
});
|
||||
expect(out.get("ok1")).toBe("completed");
|
||||
expect(out.get("up1")).toBe("upload_pending");
|
||||
expect(out.get("gone1")).toBe("incomplete"); // unexplained drop stays retryable
|
||||
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", () => {
|
||||
@@ -94,17 +76,3 @@ describe("partitionBatchOutcome", () => {
|
||||
expect(out.get("r")).toBe("completed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeUploadPollDelayMs", () => {
|
||||
it("ramps linearly and respects the cap", () => {
|
||||
expect(computeUploadPollDelayMs(1, 1500, 8000)).toBe(1500);
|
||||
expect(computeUploadPollDelayMs(2, 1500, 8000)).toBe(3000);
|
||||
expect(computeUploadPollDelayMs(3, 1500, 8000)).toBe(4500);
|
||||
expect(computeUploadPollDelayMs(9, 1500, 8000)).toBe(8000); // capped
|
||||
});
|
||||
|
||||
it("is safe on degenerate input", () => {
|
||||
expect(computeUploadPollDelayMs(0, 1500, 8000)).toBe(1500);
|
||||
expect(computeUploadPollDelayMs(-5, 1000, 4000)).toBe(1000);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user