diff --git a/supabase/functions/indexing-v3-agent/behavior.ts b/supabase/functions/indexing-v3-agent/behavior.ts index a4aeb4012e..d5fceb2533 100644 --- a/supabase/functions/indexing-v3-agent/behavior.ts +++ b/supabase/functions/indexing-v3-agent/behavior.ts @@ -57,6 +57,36 @@ export type DeferralDecision = { }; }; +export type JobStatusRpcResult = { + ok: boolean; + gate_passed: boolean; + missing: string[] | null; + status: string; +}; + +function asObjectRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; +} + +export function parseJobStatusRpcResult(row: unknown, rpcName: string): JobStatusRpcResult | null { + const direct = asObjectRecord(row); + const nested = asObjectRecord(direct?.[rpcName]); + const candidate = direct?.ok !== undefined ? direct : nested; + if (!candidate) return null; + + const ok = candidate.ok === true; + const gatePassed = candidate.gate_passed === true; + const status = typeof candidate.status === "string" ? candidate.status : "missing_result"; + const missing = + candidate.missing === null + ? [] + : Array.isArray(candidate.missing) + ? candidate.missing.map((item) => String(item)) + : ["completion_rpc_failed"]; + + return { ok, gate_passed: gatePassed, status, missing }; +} + export function completionGateFromRow(row: CompletionGateRow): CompletionGate { return { counts: { diff --git a/supabase/functions/indexing-v3-agent/index.ts b/supabase/functions/indexing-v3-agent/index.ts index 254ea19a32..1a6025edef 100644 --- a/supabase/functions/indexing-v3-agent/index.ts +++ b/supabase/functions/indexing-v3-agent/index.ts @@ -4,9 +4,11 @@ import { completionGateFromRow, deferralDecision, missingArtifactPlan, + parseJobStatusRpcResult, shouldRunVisualArtifacts, type CompletionGate, type CompletionGateRow, + type JobStatusRpcResult, type MissingArtifactPlan, } from "./behavior.ts"; @@ -1922,14 +1924,7 @@ async function deferJob(job: ClaimedJob, gate: CompletionGate): Promise { } async function completeJob(job: ClaimedJob): Promise { - const rows = await sql< - Array<{ - ok: boolean; - gate_passed: boolean; - missing: string[] | null; - status: string; - }> - >` + const rows = await sql` select * from public.complete_strict_enrichment_job( ${job.document_id}::uuid, @@ -1939,7 +1934,7 @@ async function completeJob(job: ClaimedJob): Promise { 'visual-v3' ) `; - const result = rows[0]; + const result = parseJobStatusRpcResult(rows[0], "complete_strict_enrichment_job"); if (!result?.ok || !result.gate_passed) { throw new Error( `Strict enrichment completion blocked: ${JSON.stringify({ diff --git a/tests/indexing-v3-agent.test.ts b/tests/indexing-v3-agent.test.ts index 62b8096c53..e7ecb55f11 100644 --- a/tests/indexing-v3-agent.test.ts +++ b/tests/indexing-v3-agent.test.ts @@ -4,6 +4,7 @@ import { deferralDecision, metadataNumber, missingArtifactPlan, + parseJobStatusRpcResult, shouldRunVisualArtifacts, type CompletionGateRow, } from "../supabase/functions/indexing-v3-agent/behavior"; @@ -169,4 +170,65 @@ describe("indexing-v3-agent behavior", () => { expect(metadataNumber({ indexing_v3_agent_deferral_count: "bad" }, "indexing_v3_agent_deferral_count")).toBe(0); expect(metadataNumber(null, "indexing_v3_agent_deferral_count", 2)).toBe(2); }); + + it("parses strict completion RPC rows returned as direct table columns", () => { + const result = parseJobStatusRpcResult( + { + ok: true, + gate_passed: true, + status: "completed", + missing: [], + }, + "complete_strict_enrichment_job", + ); + + expect(result).toEqual({ + ok: true, + gate_passed: true, + status: "completed", + missing: [], + }); + }); + + it("parses strict completion RPC rows returned as nested jsonb function columns", () => { + const result = parseJobStatusRpcResult( + { + complete_strict_enrichment_job: { + ok: true, + gate_passed: true, + status: "completed", + missing: [], + }, + }, + "complete_strict_enrichment_job", + ); + + expect(result).toEqual({ + ok: true, + gate_passed: true, + status: "completed", + missing: [], + }); + }); + + it("treats null missing arrays from completion RPCs as no missing artifacts", () => { + const result = parseJobStatusRpcResult( + { + complete_strict_enrichment_job: { + ok: true, + gate_passed: true, + status: "completed", + missing: null, + }, + }, + "complete_strict_enrichment_job", + ); + + expect(result).toEqual({ + ok: true, + gate_passed: true, + status: "completed", + missing: [], + }); + }); });