Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
102 lines
7.5 KiB
TypeScript
102 lines
7.5 KiB
TypeScript
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");
|
|
});
|
|
|