Files
Jyotisha/frontend/tests/rectification-tie-break-entry-20260913.test.ts
T
Jesse_ChenandClaude Opus 5.5 ea21743b09 fix(rectification): stop spoken collect once the training gate opens (BUG-1084..1087)
Once the discriminator training gate is open, only choice cards are asked
and the range card goes out when they are exhausted; targeted lines, their
re-ask and guided windows no longer hold the card or invite more events.
Delivery body says how many choice questions were used instead of the event
fit percent; narration names an excluded cluster instead of "range
unchanged"; a delivered turn no longer carries a collect question.

Offline replay (v4, 3 radii x 2 directions): truth in range 20/20 in every
cell; guided-window injections give the same width in truth and opposite
directions, so red line 1 was revised by product to truth-in-range only.
Skill 10.0.31 -> 10.0.32 (10.0.31 kept as deprecated for pinned cases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-29 10:06:57 +08:00

209 lines
9.5 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 { readFileSync } from "node:fs";
import test from "node:test";
import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts";
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
import { isTieBreakRoundSchema } from "../src/lib/rectification-agentic/v9/choice-card.ts";
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import {
buildMethodFollowupPlan,
tieBreakPersonalityAvailable,
tieBreakPersonalityFollowup,
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { expectedAnswerSchemaFor, stableFollowupQuestionId } from "../src/lib/rectification-agentic/v9/server-focus.ts";
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
const TIMES = ["04:48", "04:53", "05:06", "05:07"] as const;
const LEDGER = [
{
status: "confirmed",
domain: "education",
datePrecision: "month",
occurredFrom: "2016-09-01",
occurredTo: "2016-09-30",
eventKind: "education_start",
summary: "上大学",
},
{
status: "confirmed",
domain: "career",
datePrecision: "month",
occurredFrom: "2020-04-01",
occurredTo: null,
eventKind: "career_entry",
summary: "入职实习",
},
{
status: "confirmed",
domain: "relationship",
datePrecision: "month",
occurredFrom: "2024-05-01",
occurredTo: null,
eventKind: "relationship_start",
summary: "确定关系",
},
] as const;
function contrastProbe(layer: "d9" | "d10") {
const signs = layer === "d9" ? ["巨蟹座", "狮子座"] as const : ["狮子座", "处女座"];
return {
probeId: `contrast:varga.${layer}.${signs[0]}/${signs[1]}`,
candidateSetVersion: "04:48-05:07",
question: layer === "d9"
? "亲密关系里更接近下面哪一种相处方式?"
: "平时做事,你更接近下面哪一种?",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["04:53"], conflictsCandidateIds: ["05:06"] },
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:06"], conflictsCandidateIds: ["04:53"] },
],
candidateSplitHash: `varga.${layer}.${signs[0]}/${signs[1]}`,
informationGain: layer === "d9" ? 1.4 : 0.9,
sourceFeatures: [{ technique: layer.toUpperCase(), calculationResultId: null }],
domain: layer === "d9" ? "relationship" : "career",
year: null,
semanticKey: `varga.${layer}.${signs[0]}/${signs[1]}`,
choiceKind: "varga_style" as const,
styleOptions: [
{ label: "相处里更在意照顾对方的感受", answerClass: "yes" as const, sign: signs[0] },
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes" as const, sign: signs[1] },
],
};
}
const D9 = contrastProbe("d9");
const D10 = contrastProbe("d10");
function planInput(askedProbeKeys: readonly string[] = []) {
return {
evidence: LEDGER,
declinedTopics: [],
sessionOutcome: "discriminate_candidates" as const,
candidatesSeparated: false,
remainingLayers: ["d9", "d10"],
remainingSplitTimes: ["04:48", "05:07"] as const,
remainingCandidateCount: 4,
topCandidateTimes: [...TIMES],
askedProbeKeys,
contrastPacket: {
candidateSetVersion: "04:48-05:07",
vargaDifferences: [],
probes: [D9, D10],
},
};
}
test("GET availability matches unasked D9/D10 varga_style, not window_scan", () => {
const open = planInput();
// 原值: 主 followup 无 choice_kind
// 新值: 未覆盖领域会先出定向存在性题
// 原因: D5 七条线都会问到;性格题仍是卡下可选入口
const followup = buildMethodFollowupPlan(open).next_followup;
if (followup?.intent === "collect_method_evidence") {
assert.equal(followup.choice_kind, "existence");
}
assert.equal(tieBreakPersonalityAvailable(open), true);
assert.match(tieBreakPersonalityFollowup(open)?.semantic_key ?? "", /varga\.d9/);
assert.equal(rangeDeliveryForSnapshot({
window_scan: { d9_candidates_differ: true, d10_candidates_differ: true },
tieBreakAvailable: tieBreakPersonalityAvailable(open),
}).tie_break_available, true);
const afterD9 = planInput([D9.semanticKey]);
const second = tieBreakPersonalityFollowup(afterD9);
assert.match(second?.semantic_key ?? "", /varga\.d10/);
assert.equal(second?.tie_break_round, true);
assert.equal(tieBreakPersonalityAvailable(afterD9), true);
const bothAsked = planInput([D9.semanticKey, D10.semanticKey]);
assert.equal(tieBreakPersonalityFollowup(bothAsked), null);
assert.equal(tieBreakPersonalityAvailable(bothAsked), false);
assert.equal(rangeDeliveryForSnapshot({
window_scan: { d9_candidates_differ: true, d10_candidates_differ: true },
tieBreakAvailable: tieBreakPersonalityAvailable(bothAsked),
}).tie_break_available, false);
assert.equal(tieBreakPersonalityAvailable({ ...open, accepted: true }), false);
});
test("one click stamps a round schema; scoring stays tie_break without elimination", () => {
const followup = tieBreakPersonalityFollowup(planInput());
assert.ok(followup?.choice_frame);
const schema = expectedAnswerSchemaFor(
followup.choice_frame,
stableFollowupQuestionId(followup),
null,
followup,
);
assert.equal(schema?.tie_break_round, true);
assert.equal(isTieBreakRoundSchema(schema), true);
assert.equal(isTieBreakRoundSchema({ choice_kind: "varga_style" }), false);
const d10: ConflictProbe = {
id: "probe:varga.d10.狮子座/处女座",
semantic_key: "varga.d10.狮子座/处女座",
candidate_split_hash: "04:48-05:07:varga.d10",
domain: "career",
year: 0,
question: "平时做事,你更接近下面哪一种?",
candidate_ids: [...TIMES],
expected_outcomes: [
{ answer_class: "yes", supports: ["04:53"], conflicts: ["05:06"] },
{ answer_class: "weak_yes", supports: ["05:06"], conflicts: ["04:53"] },
{ answer_class: "no", supports: [], conflicts: [] },
{ answer_class: "unsure", supports: [], conflicts: [] },
],
information_gain: 0.9,
source: "varga_contrast",
choice_kind: "varga_style",
style_options: [
{ label: "相处里更在意照顾对方的感受", answer_class: "yes", sign: "巨蟹座" },
{ label: "习惯带头,也不排斥站到台前", answer_class: "weak_yes", sign: "狮子座" },
],
};
const scored = applyProbeOutcome(
{ "04:48": 10, "04:53": 12, "05:06": 11, "05:07": 10 },
d10,
"yes",
);
assert.equal(scored.kind, "tie_break");
assert.equal(scored.deltas["04:53"], 1);
assert.equal(scored.eliminated_ids.length, 0);
});
test("click path does not surface 409 copy; round continue is opt-in only", () => {
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const chat = rectificationChatSurface;
const answer = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
const panel = readFileSync(new URL("../src/lib/rectification-agentic/v9/divergence-panel.ts", import.meta.url), "utf8");
const pool = readFileSync(new URL("../src/lib/rectification-agentic/v9/collection-question-pool.ts", import.meta.url), "utf8");
const route = readFileSync(
new URL("../src/app/api/rectification/cases/[caseId]/tie-break/route.ts", import.meta.url),
"utf8",
);
// 原值: 客户端 requestTieBreak 把 tie_break_unavailable 当静默刷新,不 setError
// 新值: 卡上入口已删,客户端不再有这条 click path;409 口径留在 API
// 原因: BUG-706
assert.doesNotMatch(chat, /requestTieBreak/);
assert.doesNotMatch(chat, /tie_break_unavailable/);
assert.match(route, /tie_break_unavailable/);
assert.match(answer, /continueTieBreakRound\?: boolean/);
assert.match(answer, /continueTieBreakRound === true/);
assert.match(answer, /isTieBreakRoundSchema/);
assert.doesNotMatch(panel, /d9_candidates_differ[\s\S]{0,80}tieBreakAvailable/);
assert.doesNotMatch(pool, /export function isTargetedCollectDeclined/);
assert.doesNotMatch(pool, /export function isTargetedCollectYearFollowup/);
});
test("Skill 10.0.26 lists the fourth targeted-collect skip option", () => {
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
const strategy = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/references/conversation-strategy.md", import.meta.url), "utf8");
// 原值: /^version: 10\.0\.28$/ / 新值: /^version: 10\.0\.29$/ / 原因: 年月阶段改口述并禁止追问本人
// 原值: /^version: 10\.0\.29$/ / 新值: /^version: 10\.0\.30$/ / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
// 原值: /^version: 10\.0\.30$/ / 新值: /^version: 10\.0\.31$/ / 原因: 开场改大白话、题干带例子和示例回答写进 Skill OpeningPolicy(BUG-1049,2026-09-26)
// 原值: /^version: 10\.0\.31$/ / 新值: /^version: 10\.0\.32$/ / 原因: 训练门开后只问点选卡、交付正文去吻合率写进 Skill(BUG-1084/1085,2026-09-29)
assert.match(skill, /^version: 10\.0\.32$/m);
assert.match(skill, /已拒绝(没有发生过 \/ 这类事都没有过)的目标不得换词重问;跳过的按服务器计划最多重问一次/);
assert.match(strategy, /跳过的线按服务器计划最多换一种问法再问一次/);
});