Files
Jyotisha/frontend/tests/rectification-decide-next-action.test.ts
T
jesse-uxandGrok (agent loop) 1f9947a0d7 删除三个无调用方接口和约 3000 行前端死代码
关掉未登录就会触发约 20 秒计算的入口,并记下 BUG-1342。普通导出的逐字节锁保留。自测 tsc 0 错,lint 0 error / 123 warning,改到的前端测试 fail=0,两份 Python 测试通过,test:db 失败 13 条与 Windows 已知清单一致。

Co-Authored-By: Grok (agent loop)
2026-10-14 08:39:32 +08:00

657 lines
26 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 { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import {
askedEventProbeKeysFromLedgerEvidence,
buildCandidateContrastPacket,
mentionedVargaKeysFromLedgerEvidence,
selectDiscriminatorProbe,
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { evaluateCandidateSeparation } from "../src/lib/rectification-agentic/core/candidate-separation.ts";
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
import { decideRectification, publicCanAdopt, publicDecisionFields } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
classifySnapshotStaleReason,
scoreableSnapshotIsCurrent,
snapshotIsCurrent,
storedSnapshotIsCurrent,
type CandidateSnapshotSource,
} from "../src/lib/rectification-agentic/core/snapshot-source.ts";
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { buildSkillVerificationReport } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts";
const TIED = [
{ id: "c0", time: "05:00", score: 34 },
{ id: "c1", time: "05:01", score: 33 },
{ id: "c2", time: "05:02", score: 33 },
];
const SEPARATED = [
{ id: "c0", time: "05:00", score: 62 },
{ id: "c1", time: "05:01", score: 22 },
{ id: "c2", time: "05:02", score: 16 },
];
const CONTRAST_PROBE = selectDiscriminatorProbe(buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:02:05:00,05:01,05:02",
calculationResultId: "11111111-1111-4111-8111-111111111111",
engineProbes: [{
semantic_key: "career.2018.dasha_activation",
candidate_split_hash: "career:2018:05:00|05:01",
domain: "career",
year: 2018,
user_meaning: "2018 年前后是否职责明显加重?",
information_gain: 0.21,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:01", "05:02"] },
{ answer_class: "no", supports: ["05:01"], conflicts: ["05:00"] },
],
}],
vargaDifferences: [{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] }],
}));
function source(overrides: Partial<CandidateSnapshotSource> = {}): CandidateSnapshotSource {
return {
birthProfileFingerprint: "birth-a",
scoreableEvidenceFingerprint: "score-a",
inferenceRevision: 3,
candidateSetVersion: "set-a",
scoringPolicyVersion: "policy-v2",
...overrides,
};
}
test("does not adopt when method coverage is complete but candidates remain tied", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(next.separation.status, "not_separated");
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
});
test("tied candidates prefer a discriminator probe with at least two predicted outcomes", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
});
test("holdout that has not passed cannot enter ready_to_adopt", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: SEPARATED,
holdoutValidation: "not_started",
});
assert.equal(next.type, "ask_holdout_validation");
assert.notEqual(next.type, "ready_to_adopt");
});
test("separated candidates with holdout passed are ready to adopt", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: SEPARATED,
holdoutValidation: "passed",
});
assert.equal(next.type, "ready_to_adopt");
});
test("missing method coverage stays in fact collection even if scores look separated", () => {
const next = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
candidateScores: SEPARATED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_fact_collection");
});
test("unseparated scores still discriminate when training is open and a probe exists", () => {
const next = decideNextAction({
methodCoverageAll: false,
trainingGateOpen: true,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: [],
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
});
test("dated evidence suppresses an ordinary probe for the same domain and year", () => {
const askedKeys = askedEventProbeKeysFromLedgerEvidence([{
status: "confirmed",
domain: "career",
occurredFrom: "2018-01-01",
}]);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
engineProbes: [{
semantic_key: "career.2018.dasha_activation",
candidate_split_hash: "career:2018:split",
domain: "career",
year: 2018,
user_meaning: "2018 年前后是否有职业变化?",
information_gain: 0.4,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07"] },
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00"] },
],
}],
askedKeys,
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("ordinary education evidence does not suppress a structured D24 probe", () => {
const askedKeys = askedEventProbeKeysFromLedgerEvidence([{
status: "confirmed",
domain: "education",
occurredFrom: "2016-09-01",
}]);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
engineProbes: [{
semantic_key: "varga.d24",
candidate_split_hash: "d24:split",
domain: "education",
year: 2016,
choice_kind: "varga_style",
user_meaning: "这段学习经历更接近哪种过程?",
information_gain: 0.8,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07"] },
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00"] },
],
}],
askedKeys,
});
assert.equal(selectDiscriminatorProbe(packet)?.semanticKey, "varga.d24");
});
test("whole-window D9/D10 signs do not synthesize a discriminator without remaining-minute splits", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
calculationResultId: "22222222-2222-4222-8222-222222222222",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("exam quality and occupation notes keep remaining D10 and drop yearless D4/D24 from selection", () => {
const evidence = [
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员 / 前端", status: "confirmed" },
];
const mentionedKeys = mentionedVargaKeysFromLedgerEvidence(evidence);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
calculationResultId: "22222222-2222-4222-8222-222222222222",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d4", at: "05:03", from_sign: "金牛座", to_sign: "双子座" },
{ layer: "d4", at: "05:04", from_sign: "双子座", to_sign: "巨蟹座" },
{ layer: "d10", at: "05:03", from_sign: "巨蟹座", to_sign: "狮子座" },
{ layer: "d10", at: "05:04", from_sign: "狮子座", to_sign: "处女座" },
{ layer: "d24", at: "05:03", from_sign: "白羊座", to_sign: "金牛座" },
{ layer: "d24", at: "05:04", from_sign: "金牛座", to_sign: "双子座" },
],
mentionedKeys,
});
assert.ok(packet.probes.some((item) => item.semanticKey.startsWith("varga.d10.")));
assert.ok(packet.probes.some((item) => item.semanticKey.startsWith("varga.d24.")));
const probe = selectDiscriminatorProbe(packet, { mentionedKeys });
assert.ok(probe);
assert.match(probe.semanticKey, /varga\.d10/);
assert.equal(probe.domain, "career");
assert.equal(probe.choiceKind, "varga_style");
});
test("encoded D24/D10/D4 remaining splits leave no discriminator", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d4", at: "05:00" },
{ layer: "d4", at: "05:03" },
{ layer: "d10", at: "05:00" },
{ layer: "d24", at: "05:00" },
],
askedKeys: ["varga.d4", "varga.d10", "varga.d24", "varga.d5"],
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("remaining-candidate D24 split is not a selectable discriminator", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:07",
calculationResultId: "22222222-2222-4222-8222-222222222222",
vargaDifferences: [
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
],
candidateTimes: ["05:00", "05:06", "05:07"],
transitions: [
{ layer: "d10", at: "05:00" },
{ layer: "d10", at: "05:15" },
{ layer: "d24", at: "05:00" },
{ layer: "d24", at: "05:06" },
],
askedKeys: ["education.2016", "varga.d10"],
});
const probe = selectDiscriminatorProbe(packet);
assert.equal(probe, null);
});
test("user stop completes with a credible range instead of trapping on exact-minute", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "complete_with_range");
});
test("user stop skips remaining collection and holdout when candidates already exist", () => {
const incomplete = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: TIED,
});
assert.equal(incomplete.type, "complete_with_range");
const separated = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "not_started",
});
assert.equal(separated.type, "complete_with_range");
const stopped = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
userStopped: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "not_started",
});
// 原断言 sessionOutcome=provisional_range_user_stopped
// → 新断言 sessionOutcome=adopt_representative。
// coverage 不再挡代表性采用;completionStatus 仍标记用户停止。确认门仍关。
assert.equal(stopped.sessionOutcome, "adopt_representative");
assert.equal(stopped.canAdopt, true);
assert.equal(stopped.canConfirmExactMinute, false);
assert.equal(stopped.completionStatus, "provisional_range_user_stopped");
assert.equal(stopped.validated, false);
});
// 原值: 题名「waits for three training events」/ 新值: 「waits for four dated events across three domains」/ 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
test("discrimination waits for four dated events across three domains after reserving holdout", () => {
const two = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
]);
const three = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
{ id: "e3", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
]);
const four = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
{ id: "e3", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-10-01", occurredTo: null },
{ id: "e4", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
]);
assert.equal(two.open, false);
assert.equal(two.holdoutCount, 0);
assert.equal(two.trainingCount, 2);
// 原值: 3 件即预留 holdout,trainingCount=2,门关,等到第 4 件才区分
// 新值: 恰好 3 件全部进训练,门开,holdout 仅 ≥4 才预留
// 原因: BUG-647 训练门与 holdout 解耦
// 原值: three.open=true / 新值: false / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193);3 件仍全部进训练、不留 holdout
assert.equal(three.open, false);
assert.equal(three.holdoutCount, 0);
assert.equal(three.trainingCount, 3);
// 4 件 3 域:留出 1 件 holdout 后训练 3 件,门槛按训练 + holdout 计,门开
assert.equal(four.open, true);
assert.equal(four.trainingCount, 3);
assert.equal(four.holdoutCount, 1);
// R1(BUG-1193):4 件但只有 2 域,门仍关
const fourTwoDomains = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
{ id: "e3", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2022-10-01", occurredTo: null },
{ id: "e4", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
]);
assert.equal(fourTwoDomains.open, false);
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: two.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_fact_collection");
// 原值: ask_candidate_discriminator / 新值: ask_fact_collection / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),3 件门未开
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: three.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_fact_collection");
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: four.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_candidate_discriminator");
});
test("holdout passed is a validated range, not a user-stop close", () => {
const validated = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "passed",
});
assert.equal(validated.sessionOutcome, "validated_range");
assert.equal(validated.completionStatus, "validated_range");
assert.equal(validated.validated, true);
assert.equal(validated.canConfirmExactMinute, false);
});
test("tied candidates with no remaining discriminator offer a provisional range", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
});
assert.equal(next.type, "offer_provisional_range");
});
test("non-scoreable occupation note does not change the scoreable evidence fingerprint", () => {
const dated = [{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
status: "confirmed" as const,
eventKind: "business_start",
domain: "career",
occurredFrom: "2026-07-19",
occurredTo: null,
datePrecision: "day" as const,
summary: "注册公司",
}];
const withNote = [...dated, {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2",
status: "confirmed" as const,
eventKind: "occupation_note",
domain: "occupation",
occurredFrom: null,
occurredTo: null,
datePrecision: "unknown" as const,
summary: "长期一直是程序员",
}];
const before = evidenceLedgerFingerprint(dated as never);
const after = evidenceLedgerFingerprint(withNote as never);
assert.equal(after, before);
const snapshot = source({ scoreableEvidenceFingerprint: before });
const current = source({ scoreableEvidenceFingerprint: after, inferenceRevision: snapshot.inferenceRevision });
assert.equal(snapshotIsCurrent(snapshot, current), true);
assert.equal(scoreableSnapshotIsCurrent(snapshot, current), true);
});
test("scoreable occupation evidence requires a new inference revision", () => {
const before = source();
const afterScoreable = source({
scoreableEvidenceFingerprint: "score-b",
inferenceRevision: before.inferenceRevision + 1,
});
assert.equal(classifySnapshotStaleReason(before, afterScoreable), "scoreable_evidence_changed");
assert.equal(afterScoreable.inferenceRevision, before.inferenceRevision + 1);
});
test("candidate cards must be created from the current inference and scoreable revisions", () => {
const current = source({ inferenceRevision: 4 });
const card = source({ inferenceRevision: 3 });
assert.equal(classifySnapshotStaleReason(card, current), "inference_revision_changed");
const matching = source({ inferenceRevision: 4 });
assert.equal(snapshotIsCurrent(matching, current), true);
});
test("stale reasons distinguish birth profile from scoreable evidence", () => {
const current = source();
assert.equal(
classifySnapshotStaleReason(source({ birthProfileFingerprint: "birth-b" }), current),
"birth_profile_changed",
);
assert.equal(
classifySnapshotStaleReason(source({ scoreableEvidenceFingerprint: "score-b" }), current),
"scoreable_evidence_changed",
);
assert.equal(
classifySnapshotStaleReason(source({ candidateSetVersion: "set-b" }), current),
"candidate_set_superseded",
);
});
test("missing scoreable fingerprint is stale, not current", () => {
const current = source();
assert.equal(scoreableSnapshotIsCurrent(source({ scoreableEvidenceFingerprint: "" }), current), false);
assert.equal(scoreableSnapshotIsCurrent(current, source({ scoreableEvidenceFingerprint: "" })), false);
assert.equal(
classifySnapshotStaleReason(source({ scoreableEvidenceFingerprint: "" }), current),
"fingerprint_missing",
);
assert.equal(storedSnapshotIsCurrent(null, current), true);
assert.equal(storedSnapshotIsCurrent(source({ scoreableEvidenceFingerprint: "" }), current), false);
assert.equal(storedSnapshotIsCurrent(current, current), true);
});
test("stale snapshot keeps discrimination instead of returning to collection", () => {
const next = decideNextAction({
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
assert.equal(decideNextAction({
methodCoverageAll: false,
trainingGateOpen: false,
snapshotCurrent: false,
candidateScores: TIED,
}).type, "ask_fact_collection");
});
test("a single candidate is sole_candidate, not a parallel range", () => {
const separation = evaluateCandidateSeparation([{ time: "05:00", score: 80 }]);
assert.equal(separation.status, "sole_candidate");
assert.equal(separation.sufficient, true);
assert.equal(separation.lead, 0);
assert.deepEqual(separation.credibleRange, ["05:00"]);
const decision = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [{ time: "05:00", score: 80 }],
holdoutValidation: "unavailable",
});
// 原断言 sessionOutcome=provisional_range 且 canAdopt=false
// → 新断言 sessionOutcome=adopt_representative 且 canAdopt=true。
// holdout unavailable 只挡确认门;单候选仍是代表性采用,不是唯一分钟确认。
assert.equal(decision.sessionOutcome, "adopt_representative");
assert.equal(decision.canAdopt, true);
assert.equal(decision.canConfirmExactMinute, false);
assert.doesNotMatch(
buildSkillVerificationReport({
representativeTime: "05:00",
widthMinutes: 0,
candidates: [{ time: "05:00", rank: 1, relativeSupport: 80 }],
separation,
}),
/基本并列/,
);
});
test("34/33/33 is a tie, not a recommended winner", () => {
const separation = evaluateCandidateSeparation(TIED);
assert.equal(separation.status, "not_separated");
assert.equal(separation.sufficient, false);
assert.deepEqual(separation.credibleRange, ["05:00", "05:01", "05:02"]);
assert.equal(separation.representativeTime, "05:00");
});
test("final report does not claim executed techniques without calculationResultId", () => {
const report = buildSkillVerificationReport({
representativeTime: "05:00",
widthMinutes: 3,
candidates: [
{ time: "05:00", rank: 1, relativeSupport: 34 },
{ time: "05:01", rank: 2, relativeSupport: 33 },
{ time: "05:02", rank: 3, relativeSupport: 33 },
],
separation: evaluateCandidateSeparation(TIED),
techniqueAuditTable: [
{ technique: "D9", status: "executed", note: "no id" },
{ technique: "D10", status: "executed", note: "has id", calculation_result_id: "33333333-3333-4333-8333-333333333333" },
],
eventFitRate: {
matched: 8,
total: 9,
percent: 89,
label: "8/9",
user_meaning: "这段时间窗对已收事件有一定解释力",
unique_minute_claim: false,
},
});
assert.match(report, /基本并列/);
assert.match(report, /事件拟合程度,不是候选区分程度/);
assert.match(report, /\| D9 \| input_covered \|/);
assert.match(report, /\| D10 \| executed \|/);
assert.doesNotMatch(report, /当前推荐/);
});
test("MethodFollowup unions include holdout validation kinds used by next_followup", () => {
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/method-followup.ts", import.meta.url), "utf8");
const methodId = source.match(/export type MethodFollowup = Readonly<\{[\s\S]*?method_id: ([^;]+);/)?.[1] ?? "";
const askTheme = source.match(/export type MethodFollowup = Readonly<\{[\s\S]*?ask_theme: ([^;]+);/)?.[1] ?? "";
assert.match(methodId, /"holdout_validation"/);
assert.match(askTheme, /"holdout"/);
});
test("open capability with empty dated probe pool delivers instead of collecting", () => {
const collecting = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
datedMethodCollectOpen: true,
datedEventCount: 4,
datedDomainCount: 3,
trainingGateOpen: true,
snapshotCurrent: true,
candidateScores: TIED,
confirmationAllowed: false,
});
// 原值: sessionOutcome=collect_evidence,公开 can_adopt=false
// 新值: 训练门开、无带年月探针 → adopt_representative / 可出区间卡
// 原因: BUG-651 带年月池空即交付,剩余采集不得挡住结果卡
assert.equal(collecting.sessionOutcome, "adopt_representative");
assert.equal(collecting.canOfferRange, true);
assert.equal(collecting.canAdopt, true);
assert.equal(publicDecisionFields(collecting).can_adopt, true);
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "adopt_representative" }), true);
assert.equal(publicCanAdopt({ canAdopt: true, sessionOutcome: "provisional_range_user_stopped" }), true);
});
const SEPARATED_COLLECT_OPEN = {
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
datedMethodCollectOpen: true,
datedEventCount: 5,
datedDomainCount: 3,
trainingGateOpen: true,
snapshotCurrent: true,
candidateScores: SEPARATED,
holdoutValidation: "unavailable" as const,
confirmationAllowed: false,
};
test("separated candidates stay in collect while dated method collect is still open", () => {
const decision = decideRectification(SEPARATED_COLLECT_OPEN);
assert.equal(decision.sessionOutcome, "collect_evidence");
assert.equal(decision.nextAction, "ask_fact_collection");
assert.equal(publicDecisionFields(decision).can_adopt, false);
});
test("user stop still delivers while dated method collect is open", () => {
const stopped = decideRectification({
...SEPARATED_COLLECT_OPEN,
userStopped: true,
});
assert.equal(stopped.sessionOutcome, "adopt_representative");
assert.equal(stopped.nextAction, "complete_with_range");
assert.equal(stopped.completionStatus, "provisional_range_user_stopped");
assert.equal(publicDecisionFields(stopped).can_adopt, true);
});
test("accepted representative adopt ignores open dated collect", () => {
const accepted = decideRectification({
...SEPARATED_COLLECT_OPEN,
accepted: true,
});
assert.equal(accepted.sessionOutcome, "adopt_representative");
assert.equal(accepted.nextAction, "ready_to_adopt");
});