import assert from "node:assert/strict"; import test from "node:test"; import { candidateForAdoptedMinute, postAdoptCheckVerdict, readoptOffer } from "../src/lib/rectification-agentic/v9/post-adopt-verdict.ts"; import { attachOfferResultToTurns } from "../src/lib/rectification-agentic/v9/turn-question.ts"; import type { SegmentSummary } from "../src/lib/rectification-agentic/core/segment-summary.ts"; // BUG-1172 (product 10-01 option A): after a post-adopt check the reply says whether the event // fits the adopted time, and whether the chart verdicts moved — not only 「前事核对到这里」. const candidate = (time: string, range: [string, string], clusterTimes?: string[]) => ({ id: time, time, cluster_range: range, ...(clusterTimes ? { cluster_times: clusterTimes } : {}), prior_score: 10, posterior_score: 10, probability: 0.5, status: "active", rank: 1, strong_conflict_count: 0, }) as never; const CANDIDATES = [candidate("04:53", ["04:48", "04:57"], ["04:53", "04:55"]), candidate("05:03", ["05:00", "05:07"])]; const probe = { id: "probe:education.2016.09.known_event_quality:x", semantic_key: "education.2016.09.known_event_quality", candidate_split_hash: "x", domain: "education", year: 2016, question: "2016", candidate_ids: ["04:53", "05:03"], information_gain: 0.4, source: "known_event_quality", expected_outcomes: [ { answer_class: "yes", supports: ["04:53"], conflicts: ["05:03"] }, { answer_class: "no", supports: ["05:03"], conflicts: ["04:53"] }, { answer_class: "weak_yes", supports: [], conflicts: [] }, ], } as never; const summary = (tier: string, sign: number, adoptionTime: string | null = null): SegmentSummary => ({ targets: ["D1", "D9"], window_minutes: 21, scan_complete: true, no_rectification_needed: false, adoption_minute: adoptionTime ? { offset: 0, time: adoptionTime, date: "1997-08-08", signs: {} } : null, charts: [ { chart: "D1", sign: 3, tier: "certain", alternatives: [], tied: false }, { chart: "D9", sign, tier, alternatives: [], tied: false }, ], }) as never; test("the adopted minute maps to the candidate whose cluster holds it, else the nearest", () => { assert.equal(candidateForAdoptedMinute("04:55:00", CANDIDATES)?.time, "04:53"); assert.equal(candidateForAdoptedMinute("05:01", CANDIDATES)?.time, "05:03"); assert.equal(candidateForAdoptedMinute("05:30", CANDIDATES)?.time, "05:03", "outside every cluster: nearest"); }); test("verdict: fits, does not fit, cannot tell, and unsure", () => { const base = { acceptedTime: "04:55:00", probe, candidates: CANDIDATES }; assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "yes" }), "这件事和 04:55 对得上。"); // Minute product (no chart summary): the card is where a re-pick happens. assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "no" }), "这件事和 04:55 对不上;如果别的经历也对不上,可以回到上面的卡片改选。"); assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "weak_yes" }), "这件事分不出 04:55 和别的时间。"); assert.equal(postAdoptCheckVerdict({ ...base, answerClass: "unsure" }), "这件事记不清,不算。"); assert.equal(postAdoptCheckVerdict({ ...base, answerClass: null }), null); assert.equal(postAdoptCheckVerdict({ ...base, acceptedTime: null, answerClass: "yes" }), null); assert.equal(postAdoptCheckVerdict({ ...base, probe: null, answerClass: "yes" }), null, "no probe, no claim"); }); test("segment product says whether the chart verdicts moved", () => { const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "yes" as const }; assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }), "这件事和 04:55 对得上。盘型结论不变。"); // BUG-1173 — 原值: 「…盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。」 / 新值: 同一句 // 再接「你采用的 04:55 仍在其中,不用改。」 / 原因: 产品 10-01 选 A,变化时要说清采用的时间还在不在新结论里。 const minutes = [ { offset: 7, time: "04:55", signs: { D1: 3, D9: 6 } }, { offset: 14, time: "05:02", signs: { D1: 3, D9: 7 } }, ]; const moved = postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("tentative", 6), segmentMinutes: minutes }); assert.equal(moved, "这件事和 04:55 对得上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天秤(倾向,备选也要看)。你采用的 04:55 仍在其中,不用改。"); const left = postAdoptCheckVerdict({ ...base, answerClass: "no", segmentBefore: summary("credible", 6), segmentAfter: summary("credible", 7, "05:02"), segmentMinutes: minutes }); assert.equal(left, "这件事和 04:55 对不上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天蝎(较可信)。你采用的 04:55 的 D9 上升是天秤,和新的结论不一致;新的推荐时间是 05:02,可以点下面的「改用 05:02」。"); const noMinute = postAdoptCheckVerdict({ ...base, answerClass: "no", segmentBefore: summary("credible", 6), segmentAfter: summary("credible", 7), segmentMinutes: minutes }); assert.equal(noMinute, "这件事和 04:55 对不上。盘型结论有变化:D1 上升:巨蟹(确定);D9 上升:天蝎(较可信)。你采用的 04:55 的 D9 上升是天秤,和新的结论不一致,但现在还推荐不出单一时间,先按 04:55 排盘。"); for (const text of [moved!, left!, noMinute!]) assert.doesNotMatch(text, /确认|精确|相对支持|领先|落后/); }); test("a clash that does not move the verdict says so instead of inviting a re-pick", () => { const base = { acceptedTime: "04:55", probe, candidates: CANDIDATES, answerClass: "no" as const }; assert.equal(postAdoptCheckVerdict({ ...base, segmentBefore: summary("credible", 7), segmentAfter: summary("credible", 7) }), "这件事和 04:55 对不上。一件事还不足以改变盘型结论,先按 04:55 排盘。"); }); test("re-offer only when the adopted minute left the verdict and a new minute exists", () => { const minutes = [ { offset: 7, time: "04:55", signs: { D1: 3, D9: 6 } }, { offset: 14, time: "05:02", signs: { D1: 3, D9: 7 } }, ]; assert.equal(readoptOffer("04:55:00", summary("credible", 7, "05:02"), minutes), "05:02"); assert.equal(readoptOffer("04:55", summary("credible", 6, "05:02"), minutes), null, "still fits: no re-offer"); assert.equal(readoptOffer("04:55", summary("credible", 7, null), minutes), null, "no server minute: no re-offer"); assert.equal(readoptOffer("04:55", summary("indistinct", 7, "05:02"), minutes), null, "an indistinct chart does not count"); assert.equal(readoptOffer(null, summary("credible", 7, "05:02"), minutes), null); }); test("the card hangs under the latest reply only when a re-adopt is offered", () => { const turns = [ { id: "delivery", role: "assistant", question: null }, { id: "adopt", role: "assistant", question: { kind: "reverse_verify", status: "answered", answer_option: "C" } }, { id: "verdict", role: "assistant", question: null }, ] as never[]; const owner = (readopt: boolean) => attachOfferResultToTurns(turns, { resultId: "r1", canAdopt: true, acceptedTime: "04:55", readopt }) .find((turn) => (turn as { offer_result_id: string | null }).offer_result_id) as { id: string } | undefined; assert.equal(owner(false)?.id, "delivery"); assert.equal(owner(true)?.id, "verdict"); });