Files
Jyotisha/frontend/tests/rectification-midnight-date-anchor.test.ts
T

143 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
// BUG-1181: scoring-9 and later goldens must parse as dated.
// 原值: native + scoring-10;新值: 再加 scoring-11;原因: D5/D11 升到 scoring-11(BUG-1260),旧文件仍要按日期窗打开。
for (const fixture of ["rectification-midnight-date-anchor.delivery.native.json", "rectification-midnight-date-anchor.delivery.scoring-10.native.json", "rectification-midnight-date-anchor.delivery.scoring-11.native.json"]) test(`native golden crosses engine parsing, inference and dated delivery without reanchoring (${fixture})`, async () => {
const golden = JSON.parse(readFileSync(new URL(`./fixtures/${fixture}`, 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);
});