Files
Jyotisha/frontend/tests/rectification-midnight-date-anchor.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

141 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { RectificationRangeDelivery } from "../src/components/rectification-range-delivery.tsx";
import { parseRectificationCandidateResult } from "../src/lib/rectification-candidate-result.ts";
import { buildRangeDelivery } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { engineRequestBody, runV9CandidateScore } from "../src/lib/rectification-agentic/v9/engine-client.ts";
import { unionStillValidRange } from "../src/lib/rectification-agentic/core/credible-range.ts";
import { indistinguishableWidthMinutes } from "../src/lib/rectification-agentic/v9/candidate-plateau.ts";
import type { InferenceCandidate } from "../src/lib/rectification-agentic/core/types.ts";
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
import { nakshatraBoundaryProbe } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { requestCandidateIntervals, parseCandidateIntervals } from "../src/lib/rectification-agentic/core/candidate-window.ts";
import { midnightSideChoice } from "../src/lib/rectification-agentic/v9/block-scan.ts";
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
import { expectedAnswerSchemaFor } from "../src/lib/rectification-agentic/v9/server-focus.ts";
import { CHOICE_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts";
import { CASE_ID, FOCUS_ID, SESSION_ID, TURN_ID, USER_ID, activeFocusFixture, dossierFixture, fakeAccounting, receiptHandlers } from "./rectification-v9-test-support.ts";
const intervals = [{ start_index: 0, end_index: 2, start_offset_minutes: 0, end_offset_minutes: 2,
start_at: "2000-06-14T23:58", end_at: "2000-06-15T00:00", segment_index: 0 }];
test("983 request explicitly anchors midnight center on declared date", () => {
const body = engineRequestBody({ baselineBirthSnapshot: {
birth_date: "2000-06-15", reported_birth_time: "00:10", birth_time_source: "approximate",
latitude: 0, longitude: 0, timezone_offset: 0,
}, candidateRange: { start_time: "23:55", end_time: "00:25" }, 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",
}] });
assert.deepEqual(body.candidate_intervals, [{ start_at: "2000-06-14T23:55", end_at: "2000-06-15T00:25" }]);
});
test("982 credible union uses ordinal endpoints, not clock min/max", () => {
const row: InferenceCandidate = { id: "a", time: "23:59", cluster_range: ["23:58", "00:00"],
cluster_intervals: intervals, window_index: 1, window_offset_minutes: 1,
candidate_date: "2000-06-14", prior_score: 10, posterior_score: 10,
probability: 1, status: "active", rank: 1, strong_conflict_count: 0 };
assert.deepEqual(unionStillValidRange([row as InferenceCandidate]), ["23:58", "00:00"]);
});
test("nakshatra earlier/later groups follow civil ordinals across midnight", () => {
const state = buildInferenceState({ range_start: "23:58", range_end: "00:00", events: [], probes: [],
candidates: ["23:58", "23:59", "00:00"].map((time, index) => ({ id: time, time, relative_support: 50,
candidate_date: index === 2 ? "2000-06-15" : "2000-06-14", window_index: index,
window_offset_minutes: index, segment_index: 0, cluster_intervals: intervals })) });
const probe = nakshatraBoundaryProbe(state, { near_boundary: true, user_meaning: null, options: [
{ key: "A", time_bias: "earlier", traits: ["直接"] }, { key: "B", time_bias: "later", traits: ["谨慎"] },
] });
assert.deepEqual(probe?.expected_outcomes[0]?.supports, ["23:58", "23:59"]);
assert.deepEqual(probe?.expected_outcomes[1]?.supports, ["00:00"]);
});
test("narrowed previous-day window never reanchors to declaration day", () => {
assert.deepEqual(requestCandidateIntervals({ birth_date: "2000-03-01", reported_birth_time: "00:10" },
{ start_time: "23:55", end_time: "23:59" }), [{ start_at: "2000-02-29T23:55", end_at: "2000-02-29T23:59" }]);
assert.equal(parseCandidateIntervals([{ start_at: "2000-02-30T00:00", end_at: "2000-03-01T00:00" }]), null);
});
for (const option of ["A", "B", "C", "skip_probe"] as const) test(`D1 initial no-result action ${option} reaches dated RPC once`, async () => {
const range = { start_time: "23:00", end_time: "03:59", midnight_side_pending: true, candidate_intervals: [
{ start_at: "2000-06-15T00:00", end_at: "2000-06-15T03:59" },
{ start_at: "2000-06-15T23:00", end_at: "2000-06-15T23:59" },
] };
const midnight = midnightSideChoice(range)!;
const plan = buildMethodFollowupPlan({ evidence: [], candidateRange: range, caseStage: "block_scan" });
assert.equal(plan.next_followup?.midnight_side, true);
const schema = expectedAnswerSchemaFor(midnight.frame, midnight.frame.question_id, null, plan.next_followup!);
assert.ok(schema, "initial followup must produce a real renderable focus schema");
let current = dossierFixture({ stage: "block_scan", candidateRange: range, latestResult: null, evidence: [],
conversationSummary: { active_focus: activeFocusFixture({ intent: "choose_birth_block", questionId: midnight.frame.question_id,
expectedAnswerSchema: schema }), declined_skipped_topics: [] } });
const accounting = fakeAccounting({ ...receiptHandlers,
get_agentic_rectification_case_dossier: () => current,
advance_agentic_rectification_dated_window: (_fn, args) => {
const narrowed = { start_time: String(args.p_start_time), end_time: String(args.p_end_time),
candidate_intervals: args.p_candidate_intervals, midnight_side_pending: false };
current = { ...current, case: { ...current.case, candidate_range: narrowed } };
return { candidate_range: narrowed };
},
apply_agentic_rectification_choice_action: (_fn, args) => ({ status: "applied", idempotent: false, question_id: args.p_question_id,
option_id: args.p_option_id, revision: 1, narration: args.p_narration, focus_status: args.p_focus_status }),
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
set_agentic_rectification_conversation_focus: () => ({ focus: null, idempotent: false }),
});
await applyRectificationChoice(accounting.client, { userId: USER_ID, caseId: CASE_ID, sessionId: SESSION_ID,
actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", action: CHOICE_ACTION, focusId: FOCUS_ID, optionId: option, expectedRevision: 0 });
const calls = accounting.calls.filter((item) => item.fn === "advance_agentic_rectification_dated_window");
assert.equal(calls.length, 1);
assert.equal((calls[0].args.p_candidate_intervals as unknown[]).length, option === "A" || option === "B" ? 1 : 2);
assert.equal(midnightSideChoice(current.case.candidate_range), null);
});
test("native golden crosses engine parsing, inference and dated delivery without reanchoring", async () => {
const golden = JSON.parse(readFileSync(new URL("./fixtures/rectification-midnight-date-anchor.delivery.native.json", import.meta.url), "utf8"));
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(JSON.stringify(golden.response), { status: 200 })) as typeof fetch;
try {
const request = golden.request;
const range = { start_time: request.start_time, end_time: request.end_time, candidate_intervals: request.candidate_intervals };
const scored = await runV9CandidateScore({ baselineBirthSnapshot: { birth_date: request.birth_date,
latitude: request.lat, longitude: request.lon, timezone_offset: request.tz }, candidateRange: range, events: request.events });
const inference = buildCaseInferenceState({ range, candidates: scored.candidates, evidence: [], probes: [],
transitions: scored.windowScan?.transitions });
const delivery = buildRangeDelivery({ inference, publicCandidates: scored.candidates, windowScan: scored.windowScan });
const result = parseRectificationCandidateResult({ ...scored, resultId: scored.engineResultId,
decisionReceipt: { ...scored.decisionReceipt, inference_state: inference }, rangeDelivery: delivery });
assert.ok(result);
assert.equal(result.declaredBirthDate, request.birth_date);
assert.deepEqual(result.candidates.map((row) => row.candidate_date), scored.candidates.map((row) => row.candidate_date));
assert.equal(result.credibleIntervals?.[0]?.start_at, "2000-02-29T23:58");
assert.ok(delivery.columns.length > 0, "cross-midnight range must not filter out every candidate");
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
result, acceptingCandidateId: null, readonly: true, onAccept: () => undefined,
}));
assert.match(html, /前一天/);
assert.match(html, /2000-02-29/);
assert.match(html, /原填报日期保留/);
assert.doesNotMatch(html, /2000-03-02/);
} finally { globalThis.fetch = originalFetch; }
});
for (const invalid of [null, [], {}, "", [{ start_at: "2000-03-01T00:00" }]]) test(`explicit invalid intervals never fall back: ${JSON.stringify(invalid)}`, () => {
assert.throws(() => engineRequestBody({ baselineBirthSnapshot: { birth_date: "2000-03-01", latitude: 0, longitude: 0, timezone_offset: 0 },
candidateRange: { start_time: "23:58", end_time: "00:02", candidate_intervals: invalid } as never, 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",
}] }), /invalid_candidate_intervals/);
});
test("982 plateau width retains actual minutes across midnight", () => {
assert.equal(indistinguishableWidthMinutes([{ time: "23:59", rank: 1, tiedMinuteCount: 1,
clusterStart: "23:58", clusterEnd: "00:00", clusterIntervals: intervals }]), 3);
});