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.
112 lines
6.1 KiB
TypeScript
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);
|
|
}
|
|
});
|