Files
Jyotisha/frontend/tests/rectification-saved-civil-fields.test.ts
T
jesse-ux b85c4a686a
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s
fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
2026-09-21 02:55:00 +08:00

112 lines
6.1 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { acceptV9Candidate, confirmV9BirthTime } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { CASE_ID, SESSION_ID, RESULT_ID, CANDIDATE_ID, TURN_ID, USER_ID,
candidateSnapshotFixture, dossierFixture, fakeAccounting } from "./rectification-v9-test-support.ts";
// Synthetic RPC output boundary fixtures, not engine confirmation evidence.
const civil = { saved_date: "2000-06-14", saved_timezone_offset: -4, day_offset: -1, date_changed: true };
const malformed = [
{ ...civil, saved_date: "2000-02-30" },
{ ...civil, saved_timezone_offset: "-4" },
{ ...civil, saved_timezone_offset: null },
{ ...civil, saved_timezone_offset: 15 },
{ saved_date: civil.saved_date },
{ ...civil, day_offset: 2 },
{ ...civil, date_changed: false },
];
for (const kind of ["accept", "confirm"] as const) {
test(`${kind} validates saved civil output and preserves date-less legacy`, async () => {
async function invoke(fields: Record<string, unknown>) {
const accounting = fakeAccounting({
get_agentic_rectification_case_dossier: () => dossierFixture({ latestResult: candidateSnapshotFixture() }),
[`${kind}_agentic_rectification_candidate_for_case_v2`]: () => ({
success: true, saved_time: "23:55", status: kind === "accept" ? "accepted" : "confirmed",
result_id: RESULT_ID, ...fields,
}),
});
return kind === "accept"
? acceptV9Candidate(accounting.client, USER_ID, CASE_ID, RESULT_ID, CANDIDATE_ID, TURN_ID)
: confirmV9BirthTime(accounting.client, USER_ID, CASE_ID, {
resultId: RESULT_ID, candidateId: CANDIDATE_ID, requestId: TURN_ID,
consentQuote: "Confirm synthetic boundary", sourceTurnId: TURN_ID,
});
}
const valid = await invoke(civil);
assert.equal(valid.savedDate, civil.saved_date);
assert.equal(valid.savedTimezoneOffset, civil.saved_timezone_offset);
assert.equal(valid.dayOffset, -1);
assert.equal(valid.dateChanged, true);
assert.equal(Object.hasOwn(await invoke({}), "savedDate"), false);
for (const fields of malformed) await assert.rejects(invoke(fields), /invalid_saved_civil_date/);
});
}
function executeRouteRows() {
const script = `
import { mock } from 'node:test';
import { pathToFileURL } from 'node:url';
let fields = {};
const accounting = { rpc: async (name) => ({ error: null, data: name === 'get_agentic_rectification_case'
? { session_id: ${JSON.stringify(SESSION_ID)} }
: { success: true, saved_time: '23:55', status: 'accepted', result_id: ${JSON.stringify(RESULT_ID)}, ...fields } }) };
mock.module('server-only', { namedExports: {} });
mock.module('@/lib/supabase/server', { namedExports: { createServerSupabaseClient: async () => ({
auth: { getUser: async () => ({ data: { user: { id: ${JSON.stringify(USER_ID)} } }, error: null }) }
}) } });
mock.module('@/lib/supabase/admin', { namedExports: { createAdminSupabaseClient: () => accounting } });
mock.module('@/lib/product-access', { namedExports: { isProductEnabled: async () => true } });
mock.module('@/lib/rectification-agentic/v9/request-cache', { namedExports: { withRectificationRequestCache: x => x } });
mock.module('@/lib/rectification-agentic/v9/tool-service', { namedExports: {
RectificationToolServiceError: class extends Error {},
loadV9CaseDossier: async () => ({ evidence: [], latestResult: null }), evidenceLedgerFingerprint: () => 'synthetic'
} });
mock.module('@/lib/rectification-agentic/v9/result-identity', { namedExports: { assertV9ResultWritable: async () => {} } });
mock.module('@/lib/rectification-agentic/v9/interview-state', { namedExports: {
decideFromDossier: () => ({}), overlayPublicDecision: () => ({ can_adopt: true })
} });
mock.module('@/lib/rectification-agentic/core/rectification-decision', { namedExports: {
publicDecisionFields: () => ({ can_adopt: true })
} });
mock.module('@/lib/rectification-agentic/v9/answer-choice', { namedExports: { persistNextInterviewIfIdle: async () => {} } });
const { POST } = await import(pathToFileURL(process.cwd() + '/src/app/api/rectification/cases/[caseId]/candidates/accept/route.ts').href);
const results = [];
for (fields of ${JSON.stringify([civil, {}, ...malformed])}) {
const response = await POST(new Request('https://example.invalid/api/rectification/cases/synthetic/candidates/accept', {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(${JSON.stringify({
sessionId: SESSION_ID, resultId: RESULT_ID, candidateId: CANDIDATE_ID, requestId: TURN_ID,
})})
}), { params: Promise.resolve({ caseId: ${JSON.stringify(CASE_ID)} }) });
results.push({ status: response.status, body: await response.json() });
}
console.log(JSON.stringify(results));
`;
const result = spawnSync(process.execPath, ["--experimental-test-module-mocks", "--import", "tsx", "--input-type=module", "--eval", script], {
cwd: fileURLToPath(new URL("../", import.meta.url)), encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr || result.stdout);
return JSON.parse(result.stdout.trim().split("\n").at(-1)!) as { status: number; body: Record<string, unknown> }[];
}
let routeRows: ReturnType<typeof executeRouteRows>;
test("UI adoption route preserves complete civil output and date-less legacy", () => {
routeRows = executeRouteRows();
assert.equal(routeRows[0].status, 200);
assert.equal(routeRows[0].body.ok, true);
for (const [key, value] of Object.entries(civil)) assert.equal(routeRows[0].body[key], value, key);
assert.equal(routeRows[1].status, 200);
assert.equal(routeRows[1].body.ok, true);
assert.equal(Object.hasOwn(routeRows[1].body, "saved_date"), false);
});
test("UI adoption route cannot report success for malformed or partial saved civil output", () => {
routeRows ??= executeRouteRows();
for (const row of routeRows.slice(2)) {
assert.ok(row.status >= 400, JSON.stringify(row));
assert.notEqual(row.body.ok, true);
}
});