fix(rectification): keep targeted collect cards tappable after snapshot reread (BUG-669~671)
GET dropped the A–D card when the recast had no choice_frame, so a refresh left a disabled card and a collect-wait. Project from the persisted copy, rebuild the keep frame, and send a dead choice to repair-exit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -68,6 +68,7 @@ import {
|
||||
searchWindowFromSnapshot,
|
||||
caseStageFromSnapshot,
|
||||
interviewCollectWaiting,
|
||||
interviewChoiceCardUnavailable,
|
||||
interviewStopReasonFromSnapshot,
|
||||
interviewSessionOutcomeFromSnapshot,
|
||||
type RectificationCaseSnapshotPayload,
|
||||
@@ -1482,6 +1483,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
latestLiveQuestion
|
||||
&& latestLiveQuestion.options?.length === 4
|
||||
&& currentQuestion?.focus_id === latestLiveQuestion.focus_id
|
||||
&& choiceCard
|
||||
&& choiceCard.focus_id === latestLiveQuestion.focus_id
|
||||
&& (latestLiveQuestion.kind === "choice" || latestLiveQuestion.kind === "reverse_verify")
|
||||
&& !busy
|
||||
&& !readonly
|
||||
@@ -1541,12 +1544,17 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
? currentQuestion.prompt
|
||||
: null;
|
||||
const resumableCase = caseStatus !== null && isResumableStatus(caseStatus);
|
||||
const deadChoice = interviewChoiceCardUnavailable({
|
||||
questionKind: currentQuestion?.kind,
|
||||
hasChoiceCard: Boolean(choiceCard),
|
||||
});
|
||||
const liveQuestionOnMessages = Boolean(
|
||||
latestSettledAssistant
|
||||
&& latestSettledAssistant.question
|
||||
&& currentQuestion
|
||||
&& latestSettledAssistant.question.focus_id === currentQuestion.focus_id
|
||||
&& !questionIsAnswered(latestSettledAssistant.question),
|
||||
&& !questionIsAnswered(latestSettledAssistant.question)
|
||||
&& !deadChoice,
|
||||
);
|
||||
// The gap between a settled turn and its next question has two visible
|
||||
// states: `preparing` (one live timeline row, timed refetches) and
|
||||
@@ -1562,11 +1570,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const questionGap = rectificationQuestionGapState({
|
||||
liveQuestionVisible: liveQuestionOnMessages,
|
||||
questionMissing: currentQuestion === null,
|
||||
questionLoadFailed: questionSource === "unavailable",
|
||||
questionPersisted: Boolean(currentQuestion?.prompt && questionSource === "focus"),
|
||||
questionLoadFailed: questionSource === "unavailable" || deadChoice,
|
||||
questionPersisted: Boolean(currentQuestion?.prompt && questionSource === "focus") && !deadChoice,
|
||||
offerAwaitingReader: showSelectionCards && !candidateResult?.selectedTime,
|
||||
nextUserActionId,
|
||||
collectWaiting,
|
||||
collectWaiting: collectWaiting && !deadChoice,
|
||||
busy,
|
||||
readonly,
|
||||
regenerating: regeneratingMessageKey !== null,
|
||||
@@ -1765,7 +1773,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
)
|
||||
),
|
||||
);
|
||||
const embeddedCard = question ? choiceCardFromQuestion(question, liveQuestion ? choiceCard : null) : null;
|
||||
const unansweredDeadChoice = Boolean(
|
||||
question
|
||||
&& !questionIsAnswered(question)
|
||||
&& !liveQuestion
|
||||
&& (question.kind === "choice" || question.kind === "reverse_verify")
|
||||
);
|
||||
const embeddedCard = question && !unansweredDeadChoice
|
||||
? choiceCardFromQuestion(question, liveQuestion ? choiceCard : null)
|
||||
: null;
|
||||
const afterAnswer = question && displayedMessage.state === "settled"
|
||||
? (
|
||||
<div className="rectification-message-question">
|
||||
|
||||
@@ -32,6 +32,7 @@ export const TARGETED_COLLECT_OPTION_B = "没有发生过";
|
||||
export const TARGETED_COLLECT_OPTION_C = "记不太清楚";
|
||||
export const TARGETED_COLLECT_OPTION_D = "这条先跳过";
|
||||
export const TARGETED_COLLECT_WHY_USER = "答有的话再说大概年月,没有或记不清就问下一条。";
|
||||
export const TARGETED_COLLECT_KEEP_HINT = "照发服务端卡片,不要自己写题干。";
|
||||
export const FORBIDDEN_CHOICE_COPY = /外貌|体质|胎记|疤痕|伤疤|身高|体型|(?:[01]?\d|2[0-3]):[0-5]\d/;
|
||||
export const FOCUS_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
@@ -533,6 +534,12 @@ export function serverOwnedChoiceCopy(frame: RectificationChoiceFrame): AgentCho
|
||||
};
|
||||
}
|
||||
|
||||
export function isTargetedCollectChoiceSchema(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
return (value as { targeted_collect?: unknown }).targeted_collect === true
|
||||
&& parseAgentChoiceCopy(value) !== null;
|
||||
}
|
||||
|
||||
export function parseAgentChoiceCopy(value: unknown): AgentChoiceCopy | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const row = value as Record<string, unknown>;
|
||||
@@ -660,6 +667,39 @@ export function choiceCardFromPersistedVerifyCopy(input: {
|
||||
};
|
||||
}
|
||||
|
||||
export function choiceCardFromPersistedTargetedCopy(input: {
|
||||
copy: AgentChoiceCopy;
|
||||
questionId: string;
|
||||
methodId: string;
|
||||
probeId: string | null;
|
||||
caseRevision: number | null;
|
||||
focusId: string;
|
||||
}): RectificationChoiceCard | null {
|
||||
if (!input.copy.prompt.trim() || !input.questionId.trim()) return null;
|
||||
if (!isPersistedFocusId(input.focusId)) return null;
|
||||
return {
|
||||
question_id: input.questionId,
|
||||
method_id: input.methodId,
|
||||
prompt: input.copy.prompt,
|
||||
why: "",
|
||||
varga: null,
|
||||
choice_mode: CHOICE_MODE,
|
||||
options: input.copy.options.map((option) => ({
|
||||
...option,
|
||||
role: "primary" as const,
|
||||
})),
|
||||
stop_label: CHOICE_SKIP_QUESTION_LABEL,
|
||||
stop_message: CHOICE_SKIP_QUESTION_MESSAGE,
|
||||
scoring: false,
|
||||
probe_id: input.probeId,
|
||||
case_revision: input.caseRevision,
|
||||
focus_id: input.focusId,
|
||||
why_user: TARGETED_COLLECT_WHY_USER,
|
||||
answer_impact: emptyAnswerImpact(),
|
||||
choice_kind: "existence",
|
||||
};
|
||||
}
|
||||
|
||||
export function isHoldoutVerificationQuote(quote: string): boolean {
|
||||
return quote.includes(HOLDOUT_MESSAGE_PREFIX);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*
|
||||
* The tap card is shown when the rebuilt follow-up has a matching
|
||||
* discriminator frame, or when the open focus is already a reverse-verify /
|
||||
* out-of-sample A–D schema (even if the recast has no choice_frame).
|
||||
* out-of-sample / targeted-collect A–D schema (even if the recast has no
|
||||
* choice_frame, and even if persist added a `:next` suffix).
|
||||
* Card identity is the persisted focus UUID plus the inference revision.
|
||||
*/
|
||||
|
||||
|
||||
@@ -73,11 +73,14 @@ import {
|
||||
buildChoiceFrame,
|
||||
isPersistedFocusId,
|
||||
parseAgentChoiceCopy,
|
||||
isTargetedCollectChoiceSchema,
|
||||
preferConcreteChoicePrompt,
|
||||
mergeChoiceCard,
|
||||
choiceCardFromPersistedVerifyCopy,
|
||||
choiceCardFromPersistedTargetedCopy,
|
||||
serverOwnedChoiceCopy,
|
||||
buildTargetedCollectExistenceFrame,
|
||||
TARGETED_COLLECT_KEEP_HINT,
|
||||
type RectificationChoiceCard,
|
||||
type RectificationChoiceFrame,
|
||||
} from "./choice-card.ts";
|
||||
@@ -634,6 +637,24 @@ const PROBE_METHOD_ID = {
|
||||
health_pressure: "d30_health",
|
||||
} as const;
|
||||
|
||||
function targetedCollectDomainFromFocus(focus: {
|
||||
questionId?: string | null;
|
||||
targetDomain?: string | null;
|
||||
}): string | null {
|
||||
if (typeof focus.targetDomain === "string" && focus.targetDomain.trim()) {
|
||||
return focus.targetDomain.trim();
|
||||
}
|
||||
const match = /^collect:targeted:([^:]+)/.exec(focus.questionId?.trim() ?? "");
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function targetedCollectMethodId(domain: string | null): MethodFollowup["method_id"] {
|
||||
if (domain && domain in PROBE_METHOD_ID) {
|
||||
return PROBE_METHOD_ID[domain as keyof typeof PROBE_METHOD_ID];
|
||||
}
|
||||
return "dasha_events";
|
||||
}
|
||||
|
||||
function contrastFollowupDomain(
|
||||
domain: string | null,
|
||||
): keyof typeof REVERSE_VERIFY_THEME {
|
||||
@@ -2214,6 +2235,49 @@ export function buildMethodFollowupPlan(input: {
|
||||
const acceptedKeepHint = input.accepted
|
||||
? "当前排盘已采用该时间。现在按该分钟核对。"
|
||||
: "";
|
||||
const targetedCopy = isTargetedCollectChoiceSchema(focus.expectedAnswerSchema)
|
||||
? parseAgentChoiceCopy(focus.expectedAnswerSchema)
|
||||
: null;
|
||||
if (focus.intent === "collect_method_evidence" && targetedCopy) {
|
||||
const questionId = focus.questionId?.trim() ?? "";
|
||||
const domain = targetedCollectDomainFromFocus(focus);
|
||||
const methodId = targetedCollectMethodId(domain);
|
||||
const frame = questionId
|
||||
? buildTargetedCollectExistenceFrame({
|
||||
questionId,
|
||||
methodId,
|
||||
prompt: targetedCopy.prompt,
|
||||
})
|
||||
: null;
|
||||
if (frame) {
|
||||
return {
|
||||
methods,
|
||||
next_followup: {
|
||||
method_id: methodId,
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: domain && domain in REVERSE_VERIFY_THEME
|
||||
? REVERSE_VERIFY_THEME[domain as keyof typeof REVERSE_VERIFY_THEME]
|
||||
: "dated_event",
|
||||
domain,
|
||||
kind_hint: collectKindFromFocus(focus),
|
||||
user_prompt_hint: TARGETED_COLLECT_KEEP_HINT,
|
||||
must_not_label: false,
|
||||
choice_frame: frame,
|
||||
choice_kind: "existence",
|
||||
source: "active_focus",
|
||||
collection_key: questionId,
|
||||
spoken_prompt: targetedCopy.prompt,
|
||||
invite_more_once: false,
|
||||
},
|
||||
deferred_followup: null,
|
||||
session_outcome: sessionOutcome ?? "collect_evidence",
|
||||
stop_domain_rotation: true,
|
||||
do_not_poll: DO_NOT_POLL,
|
||||
not_in_rotation: NOT_IN_ROTATION,
|
||||
dropped_probes: rankedCatalog.dropped,
|
||||
};
|
||||
}
|
||||
}
|
||||
const keepNext = keepAcceptedFocus
|
||||
? makeFollowup((() => {
|
||||
const parsedId = parsePersistedFollowupQuestionId(focus.questionId);
|
||||
@@ -2947,15 +3011,15 @@ export function projectRectificationChoiceCard(
|
||||
const schemaCopy = parseAgentChoiceCopy(schema);
|
||||
const intent = input.activeFocus?.intent ?? "";
|
||||
const verifyOnly = intent === "reverse_verify" || intent === "out_of_sample_check";
|
||||
const schemaProbeId = schema && typeof schema === "object" && typeof (schema as { probe_id?: unknown }).probe_id === "string"
|
||||
? (schema as { probe_id: string }).probe_id
|
||||
: null;
|
||||
// Recast can drop choice_frame when the schema has no probe_year. The open
|
||||
// A–D schema is still the live question; GET must not silence it.
|
||||
const persistedVerifyCard = (): RectificationChoiceCard | null => {
|
||||
if (!verifyOnly || !schemaCopy) return null;
|
||||
const parsed = parsePersistedFollowupQuestionId(input.activeFocus?.questionId);
|
||||
const questionId = input.activeFocus?.questionId?.trim() ?? "";
|
||||
const probeId = schema && typeof schema === "object" && typeof (schema as { probe_id?: unknown }).probe_id === "string"
|
||||
? (schema as { probe_id: string }).probe_id
|
||||
: null;
|
||||
return choiceCardFromPersistedVerifyCopy({
|
||||
copy: {
|
||||
...schemaCopy,
|
||||
@@ -2965,11 +3029,31 @@ export function projectRectificationChoiceCard(
|
||||
methodId: parsed?.method_id
|
||||
?? (intent === "out_of_sample_check" ? "oos_blind" : "reverse_verify"),
|
||||
scoring: parsed?.scoring !== false,
|
||||
probeId,
|
||||
probeId: schemaProbeId,
|
||||
caseRevision: input.caseRevision ?? null,
|
||||
focusId,
|
||||
});
|
||||
};
|
||||
// Targeted collect is the first collect + A–D combination. GET must project
|
||||
// from the persisted copy even when the plan recast has no frame, and even
|
||||
// when persist added :next to the question id.
|
||||
const persistedTargetedCard = (): RectificationChoiceCard | null => {
|
||||
if (!isTargetedCollectChoiceSchema(schema) || !schemaCopy) return null;
|
||||
const questionId = input.activeFocus?.questionId?.trim() ?? "";
|
||||
return choiceCardFromPersistedTargetedCopy({
|
||||
copy: schemaCopy,
|
||||
questionId,
|
||||
methodId: targetedCollectMethodId(targetedCollectDomainFromFocus({
|
||||
questionId,
|
||||
targetDomain: input.activeFocus?.targetDomain,
|
||||
})),
|
||||
probeId: schemaProbeId,
|
||||
caseRevision: input.caseRevision ?? null,
|
||||
focusId,
|
||||
});
|
||||
};
|
||||
const targetedCard = persistedTargetedCard();
|
||||
if (targetedCard) return targetedCard;
|
||||
const followup = plan.next_followup ?? plan.deferred_followup ?? null;
|
||||
if (!followup) return persistedVerifyCard();
|
||||
const frame = followup.choice_frame;
|
||||
|
||||
@@ -347,6 +347,15 @@ export function interviewSessionOutcomeFromSnapshot(payload: RectificationCaseSn
|
||||
return null;
|
||||
}
|
||||
|
||||
export function interviewChoiceCardUnavailable(input: Readonly<{
|
||||
questionKind?: string | null;
|
||||
hasChoiceCard: boolean;
|
||||
}>): boolean {
|
||||
// A choice question without a GET card is a dead tap target, not a
|
||||
// collect-wait. The repair-exit path must take over.
|
||||
return input.questionKind === "choice" && !input.hasChoiceCard;
|
||||
}
|
||||
|
||||
export function interviewCollectWaiting(input: Readonly<{
|
||||
stopReason?: string | null;
|
||||
sessionOutcome?: string | null;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
rectificationQuestionGapState,
|
||||
rectificationQuestionRetryActive,
|
||||
interviewCollectWaiting,
|
||||
interviewChoiceCardUnavailable,
|
||||
interviewStopReasonFromSnapshot,
|
||||
RECTIFICATION_COLLECT_WAITING_PLACEHOLDER,
|
||||
rectificationReadonlyRangeCopy,
|
||||
@@ -127,6 +128,12 @@ test("question gap: a persisted focus question is shown instead of preparing", (
|
||||
assert.equal(rectificationQuestionRetryActive("persisted_question"), false);
|
||||
});
|
||||
|
||||
test("question gap: a choice question without a card is unavailable, not collect waiting", () => {
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "choice", hasChoiceCard: false }), true);
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "choice", hasChoiceCard: true }), false);
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "collect_spoken", hasChoiceCard: false }), false);
|
||||
});
|
||||
|
||||
test("readonly range copy never says 收窄 or 才会变", () => {
|
||||
assert.equal(
|
||||
rectificationReadonlyRangeCopy({
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
CHOICE_SKIP_QUESTION_LABEL,
|
||||
TARGETED_COLLECT_KEEP_HINT,
|
||||
TARGETED_COLLECT_OPTION_A,
|
||||
TARGETED_COLLECT_OPTION_B,
|
||||
TARGETED_COLLECT_OPTION_C,
|
||||
TARGETED_COLLECT_OPTION_D,
|
||||
parseRectificationChoiceCard,
|
||||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import {
|
||||
buildMethodFollowupPlan,
|
||||
projectRectificationChoiceCard,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { projectCurrentQuestion } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
||||
import {
|
||||
interviewChoiceCardUnavailable,
|
||||
interviewCollectWaiting,
|
||||
rectificationQuestionGapState,
|
||||
} from "../src/lib/rectification-surface-state.ts";
|
||||
|
||||
const FOCUS_ID = "abababab-abab-4bab-8bab-abababababab";
|
||||
const TARGETED_PROMPT = "收入明显变过或有过大笔进出吗?";
|
||||
const TARGETED_COPY = {
|
||||
prompt: TARGETED_PROMPT,
|
||||
option_a: TARGETED_COLLECT_OPTION_A,
|
||||
option_b: TARGETED_COLLECT_OPTION_B,
|
||||
option_c: TARGETED_COLLECT_OPTION_C,
|
||||
option_d: TARGETED_COLLECT_OPTION_D,
|
||||
options: [
|
||||
{ key: "A" as const, label: TARGETED_COLLECT_OPTION_A, answer_class: "yes" as const },
|
||||
{ key: "B" as const, label: TARGETED_COLLECT_OPTION_B, answer_class: "no" as const },
|
||||
{ key: "C" as const, label: TARGETED_COLLECT_OPTION_C, answer_class: "unsure" as const },
|
||||
{ key: "D" as const, label: TARGETED_COLLECT_OPTION_D, answer_class: "weak_yes" as const },
|
||||
],
|
||||
};
|
||||
|
||||
const DATED_EVIDENCE = [
|
||||
{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2016-09-01",
|
||||
occurredTo: "2016-09-30",
|
||||
},
|
||||
{
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-04-01",
|
||||
occurredTo: null,
|
||||
},
|
||||
] as const;
|
||||
|
||||
function targetedSchema() {
|
||||
return {
|
||||
targeted_collect: true,
|
||||
collect_kind: "targeted:finance",
|
||||
scoring: false,
|
||||
choice: TARGETED_COPY,
|
||||
prompt: TARGETED_PROMPT,
|
||||
};
|
||||
}
|
||||
|
||||
function targetedFocus(questionId: string) {
|
||||
return {
|
||||
id: FOCUS_ID,
|
||||
questionId,
|
||||
intent: "collect_method_evidence",
|
||||
targetDomain: "finance",
|
||||
targetKind: "targeted:finance",
|
||||
expectedAnswerSchema: targetedSchema(),
|
||||
};
|
||||
}
|
||||
|
||||
function project(questionId: string) {
|
||||
return projectRectificationChoiceCard({
|
||||
evidence: DATED_EVIDENCE,
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
candidatesSeparated: false,
|
||||
remainingLayers: ["d9", "d10", "d2", "d11"],
|
||||
remainingSplitTimes: ["04:48", "05:07"],
|
||||
remainingCandidateCount: 5,
|
||||
activeFocus: targetedFocus(questionId),
|
||||
});
|
||||
}
|
||||
|
||||
test("GET projects a live targeted existence card from the persisted copy", () => {
|
||||
const card = project("collect:targeted:finance");
|
||||
const question = projectCurrentQuestion(targetedFocus("collect:targeted:finance"));
|
||||
assert.ok(card, "targeted existence must stay tappable after a snapshot reread");
|
||||
assert.equal(card.focus_id, FOCUS_ID);
|
||||
assert.equal(card.question_id, "collect:targeted:finance");
|
||||
assert.equal(card.prompt, TARGETED_PROMPT);
|
||||
assert.equal(card.options.length, 4);
|
||||
assert.equal(card.options[0]?.label, TARGETED_COLLECT_OPTION_A);
|
||||
assert.equal(card.scoring, false);
|
||||
assert.equal(card.stop_label, CHOICE_SKIP_QUESTION_LABEL);
|
||||
assert.equal(card.skip_this_probe, undefined);
|
||||
assert.equal(question?.kind, "choice");
|
||||
assert.equal(card.focus_id, question?.focus_id);
|
||||
assert.equal(parseRectificationChoiceCard(card)?.focus_id, FOCUS_ID);
|
||||
});
|
||||
|
||||
test("GET still projects when persist added a :next suffix", () => {
|
||||
const card = project("collect:targeted:finance:next");
|
||||
assert.ok(card, ":next must not be the identity of a targeted card");
|
||||
assert.equal(card.question_id, "collect:targeted:finance:next");
|
||||
assert.equal(card.focus_id, FOCUS_ID);
|
||||
assert.equal(card.scoring, false);
|
||||
assert.equal(card.options.length, 4);
|
||||
});
|
||||
|
||||
test("recast keep rebuilds the targeted choice_frame and does not ask the model to rewrite the stem", () => {
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: DATED_EVIDENCE,
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
candidatesSeparated: false,
|
||||
remainingLayers: ["d9", "d10", "d2", "d11"],
|
||||
remainingSplitTimes: ["04:48", "05:07"],
|
||||
remainingCandidateCount: 5,
|
||||
activeFocus: targetedFocus("collect:targeted:relocation:next"),
|
||||
});
|
||||
const followup = plan.next_followup;
|
||||
assert.ok(followup?.choice_frame, "keep must rebuild the A–D frame");
|
||||
assert.equal(followup.choice_frame.question_id, "collect:targeted:relocation:next");
|
||||
assert.equal(followup.choice_frame.scoring, false);
|
||||
assert.equal(followup.user_prompt_hint, TARGETED_COLLECT_KEEP_HINT);
|
||||
assert.doesNotMatch(followup.user_prompt_hint, /spokenPrompt 写出题干/);
|
||||
});
|
||||
|
||||
test("a choice question without a GET card is the repair path, not collect waiting", () => {
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "choice", hasChoiceCard: false }), true);
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "choice", hasChoiceCard: true }), false);
|
||||
assert.equal(interviewChoiceCardUnavailable({ questionKind: "collect_spoken", hasChoiceCard: false }), false);
|
||||
assert.equal(
|
||||
interviewCollectWaiting({
|
||||
sessionOutcome: "collect_evidence",
|
||||
questionMissing: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
rectificationQuestionGapState({
|
||||
liveQuestionVisible: false,
|
||||
questionMissing: false,
|
||||
questionLoadFailed: true,
|
||||
questionPersisted: false,
|
||||
collectWaiting: false,
|
||||
busy: false,
|
||||
readonly: false,
|
||||
regenerating: false,
|
||||
snapshotLoaded: true,
|
||||
resumableCase: true,
|
||||
retryAttempts: 0,
|
||||
}),
|
||||
"unavailable",
|
||||
);
|
||||
const chat = readFileSync(new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url), "utf8");
|
||||
assert.match(chat, /interviewChoiceCardUnavailable/);
|
||||
assert.match(chat, /questionSource === "unavailable" \|\| deadChoice/);
|
||||
assert.match(chat, /\/api\/rectification\/cases\/\$\{encodeURIComponent\(caseId\)\}\/repair-exit/);
|
||||
assert.match(chat, /unansweredDeadChoice/);
|
||||
});
|
||||
Reference in New Issue
Block a user