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.
185 lines
5.9 KiB
TypeScript
185 lines
5.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
applyLiveCandidateOffer,
|
|
attachOfferResultToTurns,
|
|
nextSelectionCardLock,
|
|
persistedOfferFromTurn,
|
|
resolveSelectionCardMessageKey,
|
|
type TurnQuestion,
|
|
} from "../src/lib/rectification-agentic/v9/turn-question.ts";
|
|
|
|
function collect(status: "active" | "resolved"): TurnQuestion {
|
|
return {
|
|
focus_id: "focus-career",
|
|
question_id: "collect:career:collect_method_evidence",
|
|
kind: "collect_spoken",
|
|
prompt: "工作上呢,还记得哪年入职、换工作,或职责一下子变重吗?",
|
|
options: null,
|
|
status,
|
|
answer_option: null,
|
|
probe_id: null,
|
|
};
|
|
}
|
|
|
|
test("adopt offer skips an unanswered collect question and lands on the later narration", () => {
|
|
const attached = attachOfferResultToTurns([
|
|
{ id: "collect-turn", role: "assistant", question: collect("active") },
|
|
{ id: "user-turn", role: "user", question: null },
|
|
{ id: "adopt-turn", role: "assistant", question: null },
|
|
], {
|
|
resultId: "result-1",
|
|
canAdopt: true,
|
|
acceptedTime: null,
|
|
});
|
|
assert.equal(attached[0]?.offer_result_id, null);
|
|
assert.equal(attached[2]?.offer_result_id, "result-1");
|
|
});
|
|
|
|
test("adopt offer is not pinned onto a live collect question even if it is the only assistant turn", () => {
|
|
const attached = attachOfferResultToTurns([
|
|
{ id: "collect-turn", role: "assistant", question: collect("active") },
|
|
], {
|
|
resultId: "result-1",
|
|
canAdopt: true,
|
|
acceptedTime: null,
|
|
});
|
|
assert.equal(attached[0]?.offer_result_id, null);
|
|
});
|
|
|
|
test("live offer moves off a collect message once the adopt narration exists", () => {
|
|
const next = applyLiveCandidateOffer([
|
|
{
|
|
renderKey: "collect",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "工作上呢,还记得哪年入职、换工作,或职责一下子变重吗?",
|
|
question: collect("resolved"),
|
|
candidateOffer: { resultId: "result-1" },
|
|
},
|
|
{
|
|
renderKey: "user",
|
|
role: "user",
|
|
state: "settled",
|
|
text: "2020 年 4 月实习然后 10 月离职",
|
|
},
|
|
{
|
|
renderKey: "adopt",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "再问下去也分不出更准的时间了。眼下更站得住的是 05:15。这只是代表性候选,不是已确认的唯一出生分钟。我按你说的经历认真分析过了,下面是这次的结果。",
|
|
},
|
|
], { resultId: "result-1", canOffer: true, adopted: false });
|
|
assert.equal(next[0]?.candidateOffer, undefined);
|
|
assert.deepEqual(next[2]?.candidateOffer, { resultId: "result-1" });
|
|
});
|
|
|
|
test("live offer stays on the delivery message while an unanswered question is on screen", () => {
|
|
const next = applyLiveCandidateOffer([
|
|
{
|
|
renderKey: "delivery",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "我按你说的经历认真分析过了,下面是这次的结果。",
|
|
candidateOffer: { resultId: "result-1" },
|
|
},
|
|
{
|
|
renderKey: "collect",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "亲密关系里,你更接近哪一种相处方式?",
|
|
question: collect("active"),
|
|
},
|
|
], { resultId: "result-1", canOffer: true, adopted: false });
|
|
// 原值: liveInterview 时剥掉每一条 candidateOffer,交付卡消失
|
|
// 新值: 交付消息上的 offer 留下,只抑制采用动作
|
|
// 原因: BUG-706,有活题时卡必须还在
|
|
assert.deepEqual(next[0]?.candidateOffer, { resultId: "result-1" });
|
|
assert.equal(next[1]?.candidateOffer, undefined);
|
|
});
|
|
|
|
test("after adopt the offer stays on the original message", () => {
|
|
const next = applyLiveCandidateOffer([
|
|
{
|
|
renderKey: "offer",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "我按你说的经历认真分析过了,下面是这次的结果。",
|
|
candidateOffer: { resultId: "result-1" },
|
|
},
|
|
{
|
|
renderKey: "verify",
|
|
role: "assistant",
|
|
state: "settled",
|
|
text: "2021 年前后,有没有家人结婚、添丁或住院?",
|
|
question: {
|
|
focus_id: "verify",
|
|
question_id: "reverse_verify:family:score",
|
|
kind: "reverse_verify",
|
|
prompt: "2021 年前后,有没有家人结婚、添丁或住院?",
|
|
options: null,
|
|
status: "active",
|
|
answer_option: null,
|
|
probe_id: null,
|
|
},
|
|
},
|
|
], { resultId: "result-2", canOffer: true, adopted: true });
|
|
assert.deepEqual(next[0]?.candidateOffer, { resultId: "result-2" });
|
|
assert.equal(next[1]?.candidateOffer, undefined);
|
|
});
|
|
|
|
test("hydrated turns drop a stale client offer when GET says this turn is not the owner", () => {
|
|
assert.equal(
|
|
persistedOfferFromTurn(null, { resultId: "result-1" }, true),
|
|
undefined,
|
|
);
|
|
assert.deepEqual(
|
|
persistedOfferFromTurn("result-2", { resultId: "result-1" }, true),
|
|
{ resultId: "result-2" },
|
|
);
|
|
assert.deepEqual(
|
|
persistedOfferFromTurn(null, { resultId: "result-1" }, false),
|
|
{ resultId: "result-1" },
|
|
);
|
|
});
|
|
|
|
test("the offer lock keeps the original card host after adopt clears the live key", () => {
|
|
const locked = nextSelectionCardLock(null, { resultId: "result-1", key: "offer" });
|
|
assert.deepEqual(locked, { resultId: "result-1", key: "offer" });
|
|
assert.equal(
|
|
nextSelectionCardLock(locked, { resultId: "result-1", key: "offer" }),
|
|
locked,
|
|
);
|
|
assert.deepEqual(
|
|
nextSelectionCardLock(locked, { resultId: "result-1", key: undefined }),
|
|
locked,
|
|
);
|
|
assert.equal(
|
|
nextSelectionCardLock(locked, { resultId: "result-2", key: undefined }),
|
|
null,
|
|
);
|
|
assert.equal(
|
|
resolveSelectionCardMessageKey({
|
|
persistedOfferKey: undefined,
|
|
fallbackKey: "latest",
|
|
locked,
|
|
resultId: "result-1",
|
|
selectedTime: "04:49",
|
|
canOffer: false,
|
|
}),
|
|
"offer",
|
|
);
|
|
assert.equal(
|
|
resolveSelectionCardMessageKey({
|
|
persistedOfferKey: undefined,
|
|
fallbackKey: "latest",
|
|
locked,
|
|
resultId: "result-1",
|
|
selectedTime: null,
|
|
canOffer: true,
|
|
}),
|
|
"latest",
|
|
);
|
|
});
|