fix(rectification): persist the next interview after a choice tap
Independent Staging Quality Gate / validate (push) Successful in 13m56s
Independent Staging Quality Gate / publish (push) Successful in 16m46s

Closing a discriminator used to leave GET without a card after refresh.
Write the next dated question in the same request, skip childhood career
and move probes, and do not continue a read-only turn when that question
is already persisted.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-28 16:30:09 +08:00
co-authored by Cursor
parent 8c75ebb196
commit 68f0759eef
26 changed files with 2455 additions and 311 deletions
@@ -250,7 +250,10 @@ test("usage completes or releases without hiding settlement failures", () => {
assert.match(run, /activity.changed/);
assert.match(run, /if \(!answerText\.trim\(\)\) \{[\s\S]*composeRectificationTurnNarration/);
assert.match(run, /bindSpokenToOpenQuestion/);
assert.match(run, /CHOICE_CARD_CONTINUATION_ACK/);
assert.match(run, /openQuestionPromptFromToolResult/);
assert.doesNotMatch(run, /heldSpoken/);
assert.match(run, /replace: true/);
const narration = readFileSync(
new URL("../src/lib/rectification-agentic/v9/turn-narration.ts", import.meta.url),
"utf8",
@@ -293,9 +296,9 @@ test("Agentic rectification follows the conversation tail only while the reader
assert.match(chat, /choiceCardsOpen/);
assert.match(
chat,
/useLayoutEffect\(\(\) => \{\s*choiceCardsOpen\.current = showChoiceCards;\s*updateFollowState\(\);\s*\}, \[showChoiceCards, updateFollowState\]\)/,
/useLayoutEffect\(\(\) => \{\s*choiceCardsOpen\.current = showLiveChoiceCard;\s*updateFollowState\(\);\s*\}, \[showLiveChoiceCard, updateFollowState\]\)/,
);
assert.doesNotMatch(chat, /choiceCardsOpen\.current = showChoiceCards;\s*useLayoutEffect/);
assert.doesNotMatch(chat, /choiceCardsOpen\.current = showLiveChoiceCard;\s*useLayoutEffect/);
assert.match(chat, /updateFollowState/);
assert.match(chat, /\}, \[busy, candidateResult, choiceCard, error, messages, savedTime, followLatestContent\]\);/);
const jumpLatestRule = styles.match(/\.rectification-jump-latest \{[^}]+\}/)?.[0] ?? "";
@@ -539,26 +542,31 @@ test("time-selection cards appear under the latest settled agent bubble only aft
assert.match(chat, /candidateResult\?\.selectionAllowed/);
assert.match(chat, /turnOfferedSelection/);
assert.match(chat, /rectification-offer-candidates/);
assert.match(chat, /showChoiceCards = Boolean\(\s*choiceCard[\s\S]*!offeredThisTurn[\s\S]*!busy/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*offeredSelectionOnce[\s\S]*!showChoiceCards[\s\S]*!busy/);
assert.match(chat, /showLiveChoiceCard = Boolean\(\s*choiceCard[\s\S]*answeredQuestionIds[\s\S]*!busy/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*offeredSelectionOnce[\s\S]*!showLiveChoiceCard[\s\S]*!busy/);
assert.match(
chat,
/selectionCardMessageKey = showSelectionCards && latestSettledAssistant\s*\? latestSettledAssistant\.renderKey\s*: undefined/,
);
assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/);
assert.match(messageLoop, /<RectificationChoiceCard/);
assert.match(messageLoop, /key=\{`\$\{choiceCard\.question_id\}:\$\{choiceNonce\}`\}/);
assert.match(messageLoop, /choiceAttachment/);
assert.match(messageLoop, /choiceNonce/);
assert.match(messageLoop, /answered:\$\{settledChoice\.selectedKey\}/);
assert.doesNotMatch(choiceCardComponent, /setSelectedKey\(""\)/);
assert.match(chat, /showChoiceCards = Boolean\(/);
assert.match(chat, /choiceCardUserMessage/);
assert.match(choiceCardComponent, /selectedKey/);
assert.match(choiceCardComponent, /is-answered/);
assert.match(chat, /showLiveChoiceCard = Boolean\(/);
assert.match(chat, /isStructuredChoiceUserText/);
assert.match(chat, /submitStructuredChoice\(CHOICE_ACTION/);
assert.match(chat, /submitStructuredChoice\(STOP_ACTION/);
assert.match(route, /answer_choice/);
assert.match(route, /stop_and_review/);
assert.match(route, /applyRectificationChoice\(accounting/);
assert.doesNotMatch(chat, /v9-choice-user-/);
assert.doesNotMatch(chat, /send\("message", choiceCardUserMessage/);
assert.doesNotMatch(chat, /send\("message", choiceCard\?\.stop_message/);
assert.match(chat, /CHOICE_STOP_MESSAGE/);
assert.doesNotMatch(chat, /choiceCardUserMessage/);
assert.match(caseRoute, /choice_card: choiceCardFromCaseDossier/);
assert.match(caseRoute, /overlayPublicDecision/);
assert.match(caseRoute, /interview: publicDecisionFields/);
@@ -580,7 +588,7 @@ test("does not parse suggestions from an incomplete run", () => {
});
test("live answer.delta is the model reply, not a spoken-thinking split", () => {
assert.match(chat, /raw \+= event\.text/);
assert.match(chat, /raw = event\.replace === true \? event\.text : raw \+ event\.text/);
assert.match(chat, /text: raw,/);
assert.doesNotMatch(chat, /settleRectificationSpokenAndThinking/);
assert.doesNotMatch(chat, /text: settled\.spoken/);
@@ -672,13 +680,20 @@ test("choice cards render the persisted prompt as the visible question stem", ()
assert.doesNotMatch(choiceCardComponent, /<legend className="sr-only">\{props\.card\.prompt\}<\/legend>/);
});
test("choice card answers and stop share one stacked primary list", () => {
assert.match(choiceCardComponent, /birth-time-primary-choices/);
assert.match(choiceCardComponent, /props\.card\.options\.map/);
assert.doesNotMatch(choiceCardComponent, /birth-time-special-choices/);
assert.doesNotMatch(choiceCardComponent, /is-secondary/);
});
test("adopted time offers a consultation handoff without unique-minute copy", () => {
assert.match(chat, /用这个时间看盘/);
assert.match(chat, /onStartConsultation/);
assert.match(board, /换升时刻/);
assert.match(board, /经历与大运对照/);
assert.match(chat, /<RectificationChoiceCard/);
assert.match(chat, /choiceCardUserMessage/);
assert.match(chat, /choiceAttachment/);
assert.match(page, /startConsultationAfterRectification/);
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);
assert.match(agent, /start_consultation/);