Rectification answers now advance chat_sessions.updated_at (BUG-704). A ?c= id missing from the loaded page is fetched before anyone may call it deleted (BUG-705). The range card no longer has a post-card tie-break button; live questions leave the card visible with adopt locked (BUG-706/708). Spoken copy bans 相对支持度 (BUG-709). Task docs assigned 700-704; qizheng already took 700-703.
197 lines
8.2 KiB
TypeScript
197 lines
8.2 KiB
TypeScript
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";
|
||
|
||
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();
|
||
assert.equal(buildMethodFollowupPlan(open).next_followup?.choice_kind, undefined);
|
||
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", () => {
|
||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||
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");
|
||
assert.match(skill, /^version: 10\.0\.26$/m);
|
||
assert.match(skill, /有 \/ 没有 \/ 记不清 \/ 这条先跳过/);
|
||
assert.match(strategy, /有 \/ 没有 \/ 记不清 \/ 这条先跳过/);
|
||
});
|