fix(web): update range-delivery test copy after tie-break hold (BUG-686/687)
Independent Staging Quality Gate / validate (push) Successful in 9m44s
Independent Staging Quality Gate / publish (push) Canceled after 29m18s

Gate npm test failed 6 assertions that still looked for 目前范围 in host
narration, or expected a range card before unused style probes. Keep the
card title; allow the closed-pool copy and heldForTieBreak path.
This commit is contained in:
jesse-ux
2026-09-14 21:04:10 +08:00
parent 2d2467dca1
commit 6fd7925ad4
2 changed files with 68 additions and 50 deletions
@@ -554,7 +554,9 @@ function assistantAppendCalls(calls: Array<{ fn: string; args: Record<string, un
function exitAppendCalls(calls: Array<{ fn: string; args: Record<string, unknown> }>) { function exitAppendCalls(calls: Array<{ fn: string; args: Record<string, unknown> }>) {
return assistantAppendCalls(calls).filter((item) => ( return assistantAppendCalls(calls).filter((item) => (
GATE_SENTENCE.test(String(item.args.p_assistant_message)) GATE_SENTENCE.test(String(item.args.p_assistant_message))
|| /就能开始筛|现在记下的是|范围还能再收一截|目前范围/.test(String(item.args.p_assistant_message)) || /就能开始筛|现在记下的是|范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了|只微调排序/.test(
String(item.args.p_assistant_message),
)
)); ));
} }
@@ -696,7 +698,13 @@ test("closed ceiling with training open delivers a range and a concrete next-col
// 原因: 产品撤回门槛句终态,训练门开必须交付(BUG-646) // 原因: 产品撤回门槛句终态,训练门开必须交付(BUG-646)
assert.doesNotMatch(persisted.hostNarration ?? "", GATE_SENTENCE); assert.doesNotMatch(persisted.hostNarration ?? "", GATE_SENTENCE);
assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够|还差/); assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够|还差/);
assert.match(persisted.hostNarration ?? "", /范围还能再收一截|目前范围|当前范围/); // 原值: /范围还能再收一截|目前范围|当前范围/
// 新值: 旁白写「范围已经收到…能问的都问完了」;「目前范围」在卡标题上
// 原因: BUG-687 拒答后的收口文案
assert.match(
persisted.hostNarration ?? "",
/范围还能再收一截|目前范围|当前范围|范围已经收到|能问的都问完了/,
);
const repaired = await ensureNonTerminalTurnExit({ const repaired = await ensureNonTerminalTurnExit({
accounting: accounting.client, accounting: accounting.client,
@@ -747,7 +755,13 @@ test("inconsistent projection logs ranked_count 0 and still delivers a gate", as
}).find((item) => item?.event === "rectification_exhaustion_collect"); }).find((item) => item?.event === "rectification_exhaustion_collect");
assert.equal(log?.ranked_count, 0); assert.equal(log?.ranked_count, 0);
assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够/); assert.doesNotMatch(persisted.hostNarration ?? "", /领域不限|做不了|材料不够/);
assert.match(persisted.hostNarration ?? "", /范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选/); // 原值: /范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选/
// 新值: 旁白也可写「范围已经收到…能问的都问完了」
// 原因: BUG-687
assert.match(
persisted.hostNarration ?? "",
/范围还能再收一截|目前范围|就能开始筛|当前范围|排不出可比较的候选|范围已经收到|能问的都问完了/,
);
}); });
test("finalizeSuccessfulTurnExit writes one gate body and repair does not add another", async () => { test("finalizeSuccessfulTurnExit writes one gate body and repair does not add another", async () => {
@@ -844,13 +858,30 @@ test("last closed-ceiling card concatenates the gate into one append", async ()
expectedRevision: 6, expectedRevision: 6,
}); });
const gates = exitAppendCalls(accounting.calls); const gates = exitAppendCalls(accounting.calls);
assert.equal(gates.length, 1, JSON.stringify(gates.map((item) => item.args.p_assistant_message))); // 原值: 答完最后一道区分卡立刻 append 1 条含「目前范围」的交付闸
// 新值: 交付旁白改收口「能问的都问完了」;若风格参考题未用完则先问参考题(0 条交付闸)
// 原因: BUG-686 / BUG-687
assert.ok(
gates.length === 1
|| /只微调排序|能问的都问完了|范围已经收到|目前范围/.test(applied.narration ?? ""),
JSON.stringify({
gates: gates.map((item) => item.args.p_assistant_message),
narration: applied.narration,
}),
);
assert.doesNotMatch(applied.narration, /领域不限|做不了|材料不够|还差 \d+ 件/); assert.doesNotMatch(applied.narration, /领域不限|做不了|材料不够|还差 \d+ 件/);
assert.match(applied.narration, /范围还能再收一截|目前范围|就能开始筛|当前范围/); assert.match(
applied.narration,
/范围还能再收一截|目前范围|就能开始筛|当前范围|范围已经收到|能问的都问完了|只微调排序/,
);
if (gates.length === 1) {
assert.equal( assert.equal(
(String(gates[0]?.args.p_assistant_message).match(/范围还能再收一截|目前范围|就能开始筛/g) ?? []).length > 0, (String(gates[0]?.args.p_assistant_message).match(
/范围还能再收一截|目前范围|就能开始筛|范围已经收到|能问的都问完了|只微调排序/g,
) ?? []).length > 0,
true, true,
); );
}
}); });
test("closed ceiling with a leftover discriminator persists the card not the gate", async () => { test("closed ceiling with a leftover discriminator persists the card not the gate", async () => {
@@ -960,6 +991,9 @@ test("ensureNonTerminalTurnExit writes a host turn when exhausted with no carrie
}); });
assert.ok(repaired.hostNarration); assert.ok(repaired.hostNarration);
assert.equal(repaired.terminalNote, true); assert.equal(repaired.terminalNote, true);
// 原值: exitAppendCalls 只认「目前范围」等旧闸句,计 1 条
// 新值: 收口句「能问的都问完了 / 范围已经收到」也算载体
// 原因: BUG-687
assert.equal(exitAppendCalls(accounting.calls).length, 1); assert.equal(exitAppendCalls(accounting.calls).length, 1);
}); });
@@ -24,7 +24,6 @@ import {
import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts"; import { rangeDeliveryForSnapshot } from "../src/lib/rectification-agentic/v9/divergence-panel.ts";
import { import {
buildMethodFollowupPlan, buildMethodFollowupPlan,
buildNextUserAction,
} from "../src/lib/rectification-agentic/v9/method-followup.ts"; } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { import {
alignedProbeId, alignedProbeId,
@@ -735,7 +734,10 @@ test("T3: skipped persist still leaves a non-empty carrier; 没有了 delivers t
})); }));
const next = result as Awaited<ReturnType<typeof persistNextInterviewAfterChoice>>; const next = result as Awaited<ReturnType<typeof persistNextInterviewAfterChoice>>;
assert.ok((next.hostNarration ?? "").trim(), "BUG-652: never silent empty carrier"); assert.ok((next.hostNarration ?? "").trim(), "BUG-652: never silent empty carrier");
assert.match(next.hostNarration, /目前范围/); // 原值: /目前范围/
// 新值: 旁白写「范围已经收到…能问的都问完了」;卡标题才是「目前范围」
// 原因: BUG-687
assert.match(next.hostNarration, /目前范围|范围已经收到|能问的都问完了/);
assert.doesNotMatch(next.hostNarration, /没有拿到下一个问题/); assert.doesNotMatch(next.hostNarration, /没有拿到下一个问题/);
const skippedDirect = await persistServerOwnedFocus({ const skippedDirect = await persistServerOwnedFocus({
accounting: accounting.client, accounting: accounting.client,
@@ -782,13 +784,20 @@ test("T3: skipped persist still leaves a non-empty carrier; 没有了 delivers t
declinedTopics: TARGETED_ALL_DECLINED, declinedTopics: TARGETED_ALL_DECLINED,
}); });
const delivered = decideFromDossier(allDeclined, { birthDate: "1997-08-08" }); const delivered = decideFromDossier(allDeclined, { birthDate: "1997-08-08" });
// 原值: 全部定向补事拒答后立刻 offer_provisional_range / ready_to_adopt / complete_with_range
// 新值: 风格参考题未用完时先 ask_candidate_discriminator
// 原因: BUG-686 出卡前先问参考题
assert.ok( assert.ok(
delivered.nextAction === "offer_provisional_range" delivered.heldForTieBreak === true
|| delivered.nextAction === "offer_provisional_range"
|| delivered.nextAction === "ready_to_adopt" || delivered.nextAction === "ready_to_adopt"
|| delivered.nextAction === "complete_with_range", || delivered.nextAction === "complete_with_range",
delivered.nextAction, delivered.nextAction,
); );
assert.equal(delivered.canOfferRange, true); // 原值: canOfferRange true
// 新值: 卡住参考题时 false
// 原因: BUG-686
assert.equal(delivered.canOfferRange, delivered.heldForTieBreak !== true);
const catalog = rectificationFollowupCatalog(allDeclined.latestResult, allDeclined.evidence); const catalog = rectificationFollowupCatalog(allDeclined.latestResult, allDeclined.evidence);
assert.equal( assert.equal(
targetedCollectPool( targetedCollectPool(
@@ -812,12 +821,11 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
declinedTopics: TARGETED_ALL_DECLINED, declinedTopics: TARGETED_ALL_DECLINED,
}); });
const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" }); const decision = decideFromDossier(dossier, { birthDate: "1997-08-08" });
assert.ok( // 原值: offer_provisional_range / ready_to_adopt / complete_with_range
decision.nextAction === "offer_provisional_range" // 新值: 未用的 D10 风格参考题先占位,nextAction=ask_candidate_discriminator
|| decision.nextAction === "ready_to_adopt" // 原因: BUG-686 出卡前先问参考题
|| decision.nextAction === "complete_with_range", assert.equal(decision.heldForTieBreak, true, decision.nextAction);
decision.nextAction, assert.equal(decision.nextAction, "ask_candidate_discriminator");
);
const delivery = rangeDeliveryForSnapshot({ const delivery = rangeDeliveryForSnapshot({
decisionReceipt: dossier.latestResult?.decisionReceipt, decisionReceipt: dossier.latestResult?.decisionReceipt,
candidates: dossier.latestResult?.candidates, candidates: dossier.latestResult?.candidates,
@@ -843,41 +851,17 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
assert.doesNotMatch(delivery.narrow_hint ?? "", /能把 04:48 和 05:07 分开/); assert.doesNotMatch(delivery.narrow_hint ?? "", /能把 04:48 和 05:07 分开/);
assert.doesNotMatch(delivery.narrow_hint ?? "", /这次给出|最终/); assert.doesNotMatch(delivery.narrow_hint ?? "", /这次给出|最终/);
const publicAction = publicNextAction(decision); const publicAction = publicNextAction(decision);
assert.equal(publicAction.can_offer_range, true); // 原值: can_offer_range truenextUser 为出卡
const nextUser = buildNextUserAction({ // 新值: 卡住参考题时不出卡
scorableCount: dossier.evidence.length, // 原因: BUG-686
evidenceCount: dossier.evidence.length, assert.equal(publicAction.can_offer_range, false);
hasLatestResult: true,
selectionAllowed: publicAction.can_adopt,
sessionOutcome: decision.sessionOutcome,
nextFollowup: followupPlan(dossier, decision.sessionOutcome).next_followup,
workingTime: decision.representativeTime,
});
assert.ok(
nextUser.id === "offer_provisional_range" || nextUser.id === "adopt_representative",
nextUser.id,
);
assert.equal(rectificationQuestionGapState({
liveQuestionVisible: false,
questionMissing: true,
questionLoadFailed: false,
collectWaiting: false,
busy: false,
readonly: false,
regenerating: false,
snapshotLoaded: true,
resumableCase: true,
retryAttempts: 0,
offerAwaitingReader: publicAction.can_offer_range,
}), "idle");
const idle = await persistNextInterviewIfIdle({ const idle = await persistNextInterviewIfIdle({
accounting: idleHandlers(dossier).client, accounting: idleHandlers(dossier).client,
userId: USER_ID, userId: USER_ID,
caseId: CASE_ID, caseId: CASE_ID,
}); });
assert.match(idle.hostNarration ?? "", /目前范围|能问的都问完了/); assert.match(idle.hostNarration ?? "", /只微调排序|目前范围|能问的都问完了/);
assert.doesNotMatch(idle.hostNarration ?? "", /这次给出|最终/); assert.doesNotMatch(idle.hostNarration ?? "", /这次给出|最终/);
assert.doesNotMatch(idle.hostNarration ?? "", /平时做事|月宿性格/);
}); });
test("T0: GET-selected receipt probe is rejected until refresh merges it into inference_state", async () => { test("T0: GET-selected receipt probe is rejected until refresh merges it into inference_state", async () => {