import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; import { engineRequestBody, RectificationEngineError } from "../src/lib/rectification-agentic/v9/engine-client.ts"; import { scoreAndPersistCurrentEvidence } from "../src/lib/rectification-agentic/v9/score-persist.ts"; const docker = spawnSync("docker", ["version"], { stdio: "ignore" }).status === 0; test("dated window RPC preserves original midnight anchor, D1 choices and ownership", { skip: !docker && "docker unavailable" }, () => { const fixture = startPostgresFixture(); try { const migration = spawnSync(process.execPath, [fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url))], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password") }, }); assert.equal(migration.status, 0, migration.stderr); const user = randomUUID(); fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `insert into identity.users(id,name,email,email_verified) values('${user}','Fictional window','${user}@example.invalid',true)`); const makeCase = (snapshot: object, range: object) => { const id = randomUUID(), session = randomUUID(); fixture.psql(`insert into public.chat_sessions(id,user_id,title,theme,session_type,messages) values('${session}','${user}','Fictional window','general','birth_time_rectification','[]')`); fixture.psql(`insert into public.agentic_rectification_cases(id,user_id,session_id,status,stage,skill_name,skill_version,baseline_profile_fingerprint,baseline_birth_snapshot,candidate_range) values('${id}','${user}','${session}','candidate_ready','block_scan','jyotish-birth-time-rectification','9.0.0','${"a".repeat(64)}','${JSON.stringify(snapshot)}','${JSON.stringify(range)}')`); return id; }; const call = (sql: string) => JSON.parse(fixture.psql(`select ${sql}`)); const narrow = makeCase({ birth_date: "2000-03-01", reported_birth_time: "00:10", birth_time_source: "approximate", uncertainty_before_minutes: 15, uncertainty_after_minutes: 15 }, { start_time: "23:55", end_time: "23:59" }); const anchored = call(`public.ensure_agentic_rectification_dated_window('${user}','${narrow}')`); assert.deepEqual(anchored.candidate_intervals, [{ start_at: "2000-02-29T23:55", end_at: "2000-02-29T23:59" }]); for (const side of ["A", "B", "C", "skip"] as const) { const id = makeCase({ birth_date: "2000-03-01", birth_time_source: "period_only", birth_time_period: "late_night" }, { start_time: "23:00", end_time: "03:59" }); const original = call(`public.ensure_agentic_rectification_dated_window('${user}','${id}')`); assert.equal(original.midnight_side_pending, true); const parts = side === "A" ? [original.candidate_intervals[1]] : side === "B" ? [original.candidate_intervals[0]] : original.candidate_intervals; const start = side === "A" ? "23:00" : side === "B" ? "00:00" : "23:00"; const end = side === "A" ? "23:59" : "03:59"; const changed = call(`public.advance_agentic_rectification_dated_window('${user}','${id}','${start}','${end}','${JSON.stringify(parts)}',false)`); assert.deepEqual(changed.candidate_range.candidate_intervals, parts); const reloaded = call(`public.ensure_agentic_rectification_dated_window('${user}','${id}')`); assert.equal(reloaded.midnight_side_pending, false); assert.deepEqual(reloaded.candidate_intervals, parts); } assert.throws(() => call(`public.ensure_agentic_rectification_dated_window('${randomUUID()}','${narrow}')`), /case_not_found/); assert.throws(() => call(`public.advance_agentic_rectification_dated_window('${user}','${narrow}','23:55','23:59','[{"start_at":"2000-03-01T23:55","end_at":"2000-03-01T23:59"}]',false)`), /invalid_block_window/); } finally { fixture.stop(); } }); test("dated widen preserves persisted dates and late-night allowed sets transactionally", { skip: !docker && "docker unavailable" }, async (t) => { const fixture = startPostgresFixture(); try { const migration = spawnSync(process.execPath, [fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url))], { encoding: "utf8", env: { ...process.env, SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password") }, }); assert.equal(migration.status, 0, migration.stderr); const user = randomUUID(); fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `insert into identity.users(id,name,email,email_verified) values('${user}','Fictional widen','${user}@example.invalid',true)`); const snapshot = { birth_date: "2000-03-01", latitude: 0, longitude: 0, timezone_offset: 0, reported_birth_time: "00:10", birth_time_source: "approximate" }; const nightSnapshot = { ...snapshot, reported_birth_time: null, birth_time_source: "period_only", birth_time_period: "late_night" }; const both = [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T03:59" }, { start_at: "2000-03-01T23:00", end_at: "2000-03-01T23:59" }, ]; const makeCase = (range: object, birth: object = snapshot) => { const id = randomUUID(), session = randomUUID(); fixture.psql(`insert into public.chat_sessions(id,user_id,title,theme,session_type,messages) values('${session}','${user}','Fictional widen','general','birth_time_rectification','[]'); insert into public.agentic_rectification_cases(id,user_id,session_id,status,stage,skill_name,skill_version,baseline_profile_fingerprint,baseline_birth_snapshot,candidate_range) values('${id}','${user}','${session}','candidate_ready','minute','jyotish-birth-time-rectification','9.0.0','${"a".repeat(64)}','${JSON.stringify(birth)}','${JSON.stringify(range)}'); insert into public.agentic_rectification_results(user_id,session_id,case_id,engine_result_id,canonical_input_hash,algorithm_version,candidate_range,candidates,overall_confidence,baseline_birth_date,baseline_latitude,baseline_longitude,baseline_timezone_offset) values('${user}','${session}','${id}','fictional-${id}','${"b".repeat(64)}','rectification-v5-matrix-scoring-8','${JSON.stringify(range)}','[]','low','2000-03-01',0,0,0)`); return id; }; const call = (sql: string) => JSON.parse(fixture.psql(`select ${sql}`)); const widen = (id: string, start: string, end: string, caller = user) => call(`public.widen_agentic_rectification_dated_window('${caller}','${id}','${start}','${end}')`); const reload = (id: string) => call(`public.ensure_agentic_rectification_dated_window('${user}','${id}')`); const state = (id: string) => fixture.psql(`select jsonb_build_object('case',(select to_jsonb(c) from public.agentic_rectification_cases c where id='${id}'),'results',(select jsonb_agg(to_jsonb(r) order by id) from public.agentic_rectification_results r where case_id='${id}'))`); const rejectUnchanged = (id: string, start: string, end: string, error = /invalid_widen_window/, caller = user) => { const before = state(id); assert.throws(() => widen(id, start, end, caller), error); assert.equal(state(id), before, "rejected transaction changes no case/result fields, including invalidation and timestamps"); }; const advance = (id: string, start: string, end: string, parts: object[]) => { fixture.psql(`update public.agentic_rectification_cases set stage='block_scan' where id='${id}'`); return call(`public.advance_agentic_rectification_dated_window('${user}','${id}','${start}','${end}','${JSON.stringify(parts)}',false)`); }; const assertMinuteRequest = (id: string, expected: object[]) => { const range = reload(id); const birth = call(`(select baseline_birth_snapshot from public.agentic_rectification_cases where id='${id}')`); const request = engineRequestBody({ baselineBirthSnapshot: birth, candidateRange: range, events: [{ id: "00000000-0000-4000-8000-000000000001", domain: "career", event_kind: "career_entry", date_start: "2020-01-01", date_end: "2020-01-01", precision: "day", summary: "Fictional widen event", }] }); assert.deepEqual(range.candidate_intervals, expected); assert.deepEqual(request.candidate_intervals, expected, "DB reload to minute engine request keeps exact dates and holes"); }; await t.test("ordinary minute widening uses persisted previous-day anchor, not baseline re-guess", () => { const id = makeCase({ start_time: "00:00", end_time: "00:20", candidate_intervals: [{ start_at: "2000-02-29T00:00", end_at: "2000-02-29T00:20" }] }); widen(id, "00:00", "00:30"); assertMinuteRequest(id, [{ start_at: "2000-02-29T00:00", end_at: "2000-02-29T00:30" }]); }); await t.test("ordinary narrowed cross-midnight range never acquires a late-night side marker", () => { const id = makeCase({ start_time: "23:55", end_time: "00:25", candidate_intervals: [{ start_at: "2000-02-29T23:55", end_at: "2000-03-01T00:25" }] }); advance(id, "00:00", "00:20", [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:20" }]); assert.equal(Object.hasOwn(reload(id), "midnight_side_pending"), false); widen(id, "23:30", "00:30"); assertMinuteRequest(id, [{ start_at: "2000-02-29T23:30", end_at: "2000-03-01T00:30" }]); }); for (const side of ["A", "B", "C", "skip"] as const) { await t.test(`late-night ${side} keeps original allowed set through narrow/widen/reload`, () => { const id = makeCase({ start_time: "23:00", end_time: "03:59", candidate_intervals: both, midnight_side_pending: true }, nightSnapshot); const chosen = side === "A" ? [both[1]] : side === "B" ? [both[0]] : both; advance(id, side === "B" ? "00:00" : "23:00", side === "A" ? "23:59" : "03:59", chosen); const narrowed = side === "A" ? [{ start_at: "2000-03-01T23:20", end_at: "2000-03-01T23:30" }] : [{ start_at: "2000-03-01T00:10", end_at: "2000-03-01T00:20" }]; advance(id, side === "A" ? "23:20" : "00:10", side === "A" ? "23:30" : "00:20", narrowed); if (side === "A" || side === "B") { rejectUnchanged(id, side === "A" ? "23:10" : "23:30", "00:30"); widen(id, side === "A" ? "23:00" : "00:00", side === "A" ? "23:40" : "00:30"); assertMinuteRequest(id, side === "A" ? [{ start_at: "2000-03-01T23:00", end_at: "2000-03-01T23:40" }] : [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:30" }]); } else { widen(id, "23:30", "00:30"); assertMinuteRequest(id, [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:30" }, { start_at: "2000-03-01T23:30", end_at: "2000-03-01T23:59" }, ]); } assert.equal(reload(id).midnight_side_pending, false); assert.deepEqual(reload(id).midnight_allowed_intervals, chosen, "a narrowed C/skip range is not a new user side choice"); }); } await t.test("two surviving late-night segments widen without filling the daytime hole", () => { const id = makeCase({ start_time: "23:50", end_time: "00:10", candidate_intervals: [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:10" }, { start_at: "2000-03-01T23:50", end_at: "2000-03-01T23:59" }, ], midnight_side_pending: false, midnight_allowed_intervals: both }, nightSnapshot); widen(id, "23:40", "00:20"); assertMinuteRequest(id, [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:20" }, { start_at: "2000-03-01T23:40", end_at: "2000-03-01T23:59" }]); }); await t.test("real DB dossier/compute reload reaches minute score fetch with widened dated union", async (serviceTest) => { const id = makeCase({ start_time: "23:00", end_time: "03:59", candidate_intervals: both, midnight_side_pending: true }, nightSnapshot); advance(id, "23:00", "03:59", both); // D1 C/skip, not a side selection. advance(id, "00:10", "00:20", [{ start_at: "2000-03-01T00:10", end_at: "2000-03-01T00:20" }]); widen(id, "23:30", "00:30"); assert.equal(fixture.psql(`select stage from public.agentic_rectification_cases where id='${id}'`), "minute"); const turn = randomUUID(); fixture.psql(`insert into public.agentic_rectification_turns(id,case_id,status,model_name,user_message) values('${turn}','${id}','pending','fictional-test','Fictional career event'); insert into public.agentic_rectification_evidence(case_id,source_turn_id,user_quote,subject,event_kind,domain,occurred_from,occurred_to,date_precision,summary,status,confirmed_at) values('${id}','${turn}','Fictional career event','self','career_entry','career','2020-01-01','2020-01-01','day','Fictional career event','confirmed',now())`); const calls: string[] = []; const accounting = { rpc: async (fn: string, args: Record) => { calls.push(fn); assert.ok(["get_agentic_rectification_case_dossier", "get_agentic_rectification_case_compute"].includes(fn)); assert.equal(args.p_user_id, user); assert.equal(args.p_case_id, id); return { data: call(`public.${fn}('${user}','${id}')`), error: null }; } }; const keys = ["RECTIFICATION_ENGINE_VERSION", "RECTIFICATION_ALGORITHM_VERSION", "RECTIFICATION_DECISION_POLICY_VERSION"]; const saved = keys.map(key => [key, process.env[key]] as const); for (const key of keys) delete process.env[key]; serviceTest.after(() => { for (const [key, value] of saved) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }); let scoreBody: Record | null = null; serviceTest.mock.method(globalThis, "fetch", async (url: unknown, init?: RequestInit) => { if (String(url).endsWith("/versions")) return Response.json({ algorithm_version: "rectification-v5-matrix-scoring-9", decision_policy_version: "rectification-candidate-policy-v3" }); assert.ok(String(url).endsWith("/score"), "minute service must call score, not block_scan"); scoreBody = JSON.parse(String(init?.body)); // End at the network boundary: no fabricated engine contract is needed // to prove the real DB -> parser -> service -> outgoing request chain. return Response.json({ error: "fictional_network_stop_after_capture" }, { status: 400 }); }); await assert.rejects(scoreAndPersistCurrentEvidence({ accounting: accounting as never, userId: user, caseId: id }), (error: unknown) => error instanceof RectificationEngineError && error.httpStatus === 400 && error.message === "fictional_network_stop_after_capture"); assert.deepEqual(calls, ["get_agentic_rectification_case_dossier", "get_agentic_rectification_case_compute"]); assert.ok(scoreBody, "actual score fetch must be reached"); assert.equal((scoreBody as Record).birth_date, "2000-03-01"); assert.equal((scoreBody as Record).tz, 0); assert.equal((scoreBody as Record).start_time, "23:30"); assert.equal((scoreBody as Record).end_time, "00:30"); assert.deepEqual((scoreBody as Record).candidate_intervals, [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:30" }, { start_at: "2000-03-01T23:30", end_at: "2000-03-01T23:59" }, ]); }); await t.test("legacy pending=false without allowed provenance never guesses a discarded side", () => { const id = makeCase({ start_time: "00:00", end_time: "00:30", candidate_intervals: [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:30" }, ], midnight_side_pending: false }, nightSnapshot); rejectUnchanged(id, "23:30", "00:30"); assertMinuteRequest(id, [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:30" }]); const two = makeCase({ start_time: "23:50", end_time: "00:10", candidate_intervals: [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:10" }, { start_at: "2000-03-01T23:50", end_at: "2000-03-01T23:59" }, ], midnight_side_pending: false }, nightSnapshot); // Without recorded allowed provenance, retaining both existing segments // and rejecting an unverifiable expansion is the safe legacy fallback. rejectUnchanged(two, "23:40", "00:20"); assertMinuteRequest(two, [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:10" }, { start_at: "2000-03-01T23:50", end_at: "2000-03-01T23:59" }]); }); await t.test("pending D1 cannot narrow with true or null; unchanged true remains compatible", () => { for (const pending of ["true", "null"]) { const id = makeCase({ start_time: "23:00", end_time: "03:59", candidate_intervals: both, midnight_side_pending: true }, nightSnapshot); fixture.psql(`update public.agentic_rectification_cases set stage='block_scan' where id='${id}'`); const before = state(id); assert.throws(() => call(`public.advance_agentic_rectification_dated_window('${user}','${id}','00:00','03:59','${JSON.stringify([both[0]])}',${pending})`), /invalid_block_window/); assert.equal(state(id), before, "invalid pending transition rolls back every case and result field"); } const id = makeCase({ start_time: "23:00", end_time: "03:59", candidate_intervals: both, midnight_side_pending: true }, nightSnapshot); fixture.psql(`update public.agentic_rectification_cases set stage='block_scan' where id='${id}'`); const kept = call(`public.advance_agentic_rectification_dated_window('${user}','${id}','23:00','03:59','${JSON.stringify(both)}',true)`); assert.equal(kept.candidate_range.midnight_side_pending, true); assert.deepEqual(kept.candidate_range.candidate_intervals, both); assert.deepEqual(kept.candidate_range.midnight_allowed_intervals, both); }); await t.test("pending two segments preserve the gap and pending flag", () => { const id = makeCase({ start_time: "23:50", end_time: "00:10", candidate_intervals: [ { start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:10" }, { start_at: "2000-03-01T23:50", end_at: "2000-03-01T23:59" }, ], midnight_side_pending: true, midnight_allowed_intervals: both }, nightSnapshot); widen(id, "23:40", "00:20"); assertMinuteRequest(id, [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:20" }, { start_at: "2000-03-01T23:40", end_at: "2000-03-01T23:59" }]); assert.equal(reload(id).midnight_side_pending, true); }); await t.test("old containment, strictly-wider, max-width, ownership and terminal guards roll back", () => { const id = makeCase({ start_time: "00:00", end_time: "00:20", candidate_intervals: [{ start_at: "2000-03-01T00:00", end_at: "2000-03-01T00:20" }] }); rejectUnchanged(id, "00:10", "00:30"); rejectUnchanged(id, "00:00", "00:20"); rejectUnchanged(id, "00:00", "05:00"); rejectUnchanged(id, "00:00", "00:30", /case_not_found/, randomUUID()); fixture.psql(`update public.agentic_rectification_cases set status='closed',completed_at=now() where id='${id}'`); rejectUnchanged(id, "00:00", "00:30", /case_terminal/); }); } finally { fixture.stop(); } });