import assert from "node:assert/strict"; import test from "node:test"; import { RectificationCaseServiceError, adoptLegacyRectificationSkill, closeRectificationCase, getRectificationCase, getRectificationSkillIdentityStatus, getRectificationEntrySummary, loadV9RectificationProfile, mapRectificationRpcError, openRectificationCase, upgradeRectificationSkill, } from "../src/lib/rectification-agentic/v9/case-service.ts"; import type { OpenRectificationCaseRequest } from "../src/lib/rectification-agentic/v9/open-request.ts"; type RpcHandler = (fn: string, args: Record) => Promise<{ data: unknown; error: { message: string } | null; }>; function fakeAccounting(overrides: { profile?: Record | null; rpc?: RpcHandler; }) { const rpc = overrides.rpc ?? (async () => ({ data: null, error: { message: "agentic_rectification_case_not_found" } })); return { from: (table: string) => { if (table !== "profiles") throw new Error(`unexpected table ${table}`); const builder = { select: () => builder, eq: () => builder, single: async () => { if (!overrides.profile) return { data: null, error: { message: "not found" } }; return { data: overrides.profile, error: null }; }, }; return builder; }, rpc, } as never; } const completeProfile = { birth_date: "1997-08-08", birth_place_label: "河北省邯郸市", reported_birth_time: "05:00", active_birth_time: null, birth_time_source: "family_exact", birth_time_period: null, uncertainty_before_minutes: 10, uncertainty_after_minutes: 10, latitude: 36.420487, longitude: 114.209936, timezone_id: "Asia/Shanghai", timezone_offset: 8, }; const requestId = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"; const caseId = "11111111-1111-4111-8111-111111111111"; const sessionId = "22222222-2222-4222-8222-222222222222"; function openRpc(data: unknown): RpcHandler { return async () => ({ data, error: null }); } test("loadV9RectificationProfile derives baseline snapshot, fingerprint and range server-side", async () => { const accounting = fakeAccounting({ profile: completeProfile }); const profile = await loadV9RectificationProfile(accounting, "user-1"); assert.equal(profile.userId, "user-1"); assert.equal(profile.baseline.birth_date, "1997-08-08"); assert.equal(profile.baseline.birth_place_label, "河北省邯郸市"); assert.equal(profile.baseline.timezone_id, "Asia/Shanghai"); assert.equal(profile.baselineFingerprint.length, 64); assert.deepEqual(profile.candidateRange, { start_time: "04:50", end_time: "05:10" }); assert.ok(!("password" in profile.baseline)); }); test("loadV9RectificationProfile normalizes PostgreSQL Date birth dates", async () => { const dateProfile = { ...completeProfile, birth_date: new Date("1997-08-08T00:00:00.000Z"), }; const accounting = fakeAccounting({ profile: dateProfile }); const profile = await loadV9RectificationProfile(accounting, "user-1"); assert.equal(profile.baseline.birth_date, "1997-08-08"); }); test("loadV9RectificationProfile rejects incomplete profiles without creating a case", async () => { for (const incomplete of [ { ...completeProfile, latitude: null, longitude: null, timezone_offset: null }, { ...completeProfile, birth_place_label: "" }, { ...completeProfile, timezone_id: "" }, ]) { const accounting = fakeAccounting({ profile: incomplete }); await assert.rejects( () => loadV9RectificationProfile(accounting, "user-1"), (error: unknown) => error instanceof RectificationCaseServiceError && error.code === "profile_incomplete", ); } }); test("period-only profiles derive a period range; unknown derives the full day", async () => { const period = fakeAccounting({ profile: { ...completeProfile, reported_birth_time: null, birth_time_source: "period_only", birth_time_period: "morning", }, }); const periodProfile = await loadV9RectificationProfile(period, "user-1"); assert.deepEqual(periodProfile.candidateRange, { start_time: "08:00", end_time: "11:59" }); const unknown = fakeAccounting({ profile: { ...completeProfile, reported_birth_time: null, birth_time_source: "unknown", birth_time_period: null, }, }); const unknownProfile = await loadV9RectificationProfile(unknown, "user-1"); assert.deepEqual(unknownProfile.candidateRange, { start_time: "00:00", end_time: "23:59" }); }); test("homepage open with no history creates one case and one session", async () => { const accounting = fakeAccounting({ profile: completeProfile, rpc: openRpc({ disposition: "created", case_id: caseId, session_id: sessionId, status: "draft", should_start_opening: true, skill_version: "9.0.0", }), }); const request: OpenRectificationCaseRequest = { intent: "homepage", requestId }; const response = await openRectificationCase(accounting, "user-1", request); assert.equal(response.disposition, "created"); assert.equal(response.caseId, caseId); assert.equal(response.sessionId, sessionId); assert.equal(response.shouldStartOpening, true); }); test("homepage open may create a new case even when resumable history exists", async () => { const accounting = fakeAccounting({ profile: completeProfile, rpc: openRpc({ disposition: "created", case_id: caseId, session_id: sessionId, status: "draft", should_start_opening: true, skill_version: "9.0.0", }), }); const response = await openRectificationCase(accounting, "user-1", { intent: "homepage", requestId, }); assert.equal(response.disposition, "created"); assert.equal(response.shouldStartOpening, true); }); test("session intent passes the exact sessionId and never the profile snapshot", async () => { const capturedArgs: { value: Record | null } = { value: null }; const accounting = fakeAccounting({ profile: null, rpc: async (fn, args) => { capturedArgs.value = args; return { data: { disposition: "readonly", case_id: caseId, session_id: sessionId, status: "closed", should_start_opening: false, skill_version: "9.0.0", }, error: null, }; }, }); const response = await openRectificationCase(accounting, "user-1", { intent: "session", requestId, sessionId, }); assert.equal(response.disposition, "readonly"); assert.equal(capturedArgs.value?.p_session_id, sessionId); assert.deepEqual(capturedArgs.value?.p_baseline_birth_snapshot, {}); }); test("non-owner session opens surface a safe not-found error", async () => { const accounting = fakeAccounting({ profile: null, rpc: async () => ({ data: null, error: { message: "agentic_rectification_session_not_found" } }), }); await assert.rejects( () => openRectificationCase(accounting, "user-1", { intent: "session", requestId, sessionId: "99999999-9999-4999-8999-999999999999", }), (error: unknown) => error instanceof RectificationCaseServiceError && error.code === "case_session_not_found", ); }); test("legacy active-case conflicts still map to a safe public error", async () => { const accounting = fakeAccounting({ profile: completeProfile, rpc: async () => ({ data: null, error: { message: "agentic_rectification_active_case_conflict" } }), }); await assert.rejects( () => openRectificationCase(accounting, "user-1", { intent: "new", requestId }), (error: unknown) => error instanceof RectificationCaseServiceError && error.code === "active_case_conflict", ); }); test("entry summary projects resumable history for homepage context", async () => { const accounting = fakeAccounting({ rpc: async (fn) => { assert.equal(fn, "get_agentic_rectification_entry_summary"); return { data: { has_resumable_case: true, has_terminal_case_with_time: false, latest_resumable: { case_id: caseId, status: "collecting_evidence", last_activity_at: "2026-08-12T00:00:00.000Z", }, latest_terminal: null, }, error: null, }; }, }); const summary = await getRectificationEntrySummary(accounting, "user-1"); assert.equal(summary.hasResumableCase, true); assert.equal(summary.hasTerminalCaseWithTime, false); assert.equal(summary.latestResumable?.caseId, caseId); }); test("get case returns a sanitized projection without the birth snapshot", async () => { const accounting = fakeAccounting({ rpc: async () => ({ data: { case_id: caseId, session_id: sessionId, status: "candidate_ready", skill_name: "jyotish-birth-time-rectification", skill_version: "9.0.0", candidate_range: { start_time: "04:50", end_time: "05:10" }, accepted_time: null, confirmed_time: null, created_at: "2026-08-12T00:00:00.000Z", last_activity_at: "2026-08-12T00:00:00.000Z", completed_at: null, closed_reason: null, evidence_count: 3, turn_count: 4, latest_result: null, }, error: null, }), }); const view = await getRectificationCase(accounting, "user-1", caseId); assert.equal(view.caseId, caseId); assert.equal(view.evidenceCount, 3); assert.deepEqual(view.candidateRange, { start_time: "04:50", end_time: "05:10" }); assert.ok(!("baseline_birth_snapshot" in view)); assert.ok(!("birth_date" in view)); }); test("close is idempotent and never wraps as engine confirmed", async () => { const accounting = fakeAccounting({ rpc: async (fn, args) => { assert.equal(args.p_reason, "completed_by_user"); return { data: { success: true, case_id: caseId, status: "closed", idempotent: false }, error: null }; }, }); const result = await closeRectificationCase(accounting, "user-1", caseId, "completed_by_user"); assert.equal(result.status, "closed"); assert.equal(result.idempotent, false); }); test("skill identity status preserves legacy adoption state", async () => { const accounting = fakeAccounting({ rpc: async (fn) => { assert.equal(fn, "get_agentic_rectification_skill_identity_status"); return { data: { skill_name: "jyotish-birth-time-rectification", skill_version: "8.9.0", skill_sha256: null, skill_source_commit: null, skill_identity_status: "legacy_unverifiable", requires_skill_adoption: true, }, error: null, }; }, }); const status = await getRectificationSkillIdentityStatus(accounting, "user-1", caseId); assert.equal(status.status, "legacy_unverifiable"); assert.equal(status.skillSha256, null); assert.equal(status.requiresSkillAdoption, true); }); test("legacy adoption calls the explicit RPC and keeps a null previous hash", async () => { const accounting = fakeAccounting({ rpc: async (fn, args) => { assert.equal(fn, "adopt_agentic_rectification_skill_v1"); assert.equal(args.p_skill_name, "jyotish-birth-time-rectification"); assert.equal(args.p_skill_version, "9.0.0"); assert.equal(args.p_skill_sha256, "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d"); return { data: { success: true, case_id: caseId, previous_skill_name: "jyotish-birth-time-rectification", previous_skill_version: "8.9.0", previous_skill_sha256: null, previous_source_commit: null, previous_identity_status: "legacy_unverifiable", upgrade_kind: "legacy_adoption", skill_name: "jyotish-birth-time-rectification", skill_version: "9.0.0", skill_sha256: "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d", source_commit: "0fd111d16b45796086a6c1d0945dbd3de6755d8a", receipt_id: "33333333-3333-4333-8333-333333333333", idempotent: false, }, error: null, }; }, }); const result = await adoptLegacyRectificationSkill(accounting, "user-1", caseId, "9.0.0"); assert.equal(result.previousSkillSha256, null); assert.equal(result.idempotent, false); }); test("upgrade skill resolves and passes the exact registered identity", async () => { const accounting = fakeAccounting({ rpc: async (fn, args) => { assert.equal(fn, "upgrade_agentic_rectification_skill_v2"); assert.equal(args.p_skill_name, "jyotish-birth-time-rectification"); assert.equal(args.p_skill_version, "9.0.0"); assert.equal( args.p_skill_sha256, "5acb3103e80993ea611b93d2c1746e70b74aff8f8636bcffc80a837b954b470d", ); assert.equal( args.p_skill_source_commit, "0fd111d16b45796086a6c1d0945dbd3de6755d8a", ); return { data: { success: true, case_id: caseId, previous_skill_version: "8.9.0", skill_version: "9.0.0", idempotent: false, }, error: null, }; }, }); const result = await upgradeRectificationSkill(accounting, "user-1", caseId, "9.0.0"); assert.equal(result.skillVersion, "9.0.0"); }); test("rpc errors map to safe public views without leaking database text", async () => { const generic = mapRectificationRpcError(new Error("syntax error at or near \"profiles\"")); assert.equal(generic.status, 500); assert.equal(generic.code, "rectification_service_failed"); assert.doesNotMatch(generic.message, /syntax error/); const conflict = mapRectificationRpcError(new Error("agentic_rectification_active_case_conflict")); assert.equal(conflict.status, 409); assert.equal(conflict.code, "active_case_conflict"); assert.doesNotMatch(conflict.message, /agentic_rectification/); const notFound = mapRectificationRpcError(new Error("agentic_rectification_case_not_found")); assert.equal(notFound.status, 404); assert.equal(notFound.code, "case_not_found"); const publicLegacy = mapRectificationRpcError(new Error("agentic_rectification_skill_identity_unverifiable")); assert.equal(publicLegacy.status, 409); assert.equal(publicLegacy.code, "skill_identity_unverifiable"); });