fix: restore bound rectification sessions
This commit is contained in:
@@ -457,3 +457,19 @@
|
||||
- 相关记录:BUG-022、BUG-024、BUG-025
|
||||
- 复发自:无
|
||||
- 修复版本:待提交(production gate)
|
||||
|
||||
## BUG-027 | 已完成的生时校正会话打开后误建新案例
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-22
|
||||
- 最近更新:2026-07-22
|
||||
- 影响面:生时校正历史会话恢复、范围终态展示、首页继续入口与计费
|
||||
- 用户现象:范围校正完成后刷新,点击侧边栏原会话会卡在“正在建立校正记录”,或跳到另一个会话并提示加载最新进度;首页同时重新显示开始入口。
|
||||
- 触发条件:已绑定 case 的生时校正会话对应 `completed`/`abandoned` 终态,而账户接口只投影当前未完成 case;或者账户另有一个更新的未完成 case。
|
||||
- 根因:会话打开逻辑只按账户当前 case 决定 resume/start,没有优先恢复所点击会话自己的 `rectificationCaseId`;历史终态加载后又会覆盖账户当前可恢复 case,异步账户刷新还拒绝接受 `null` 或另一个最新 case。
|
||||
- 修复:已绑定的生时校正会话始终以自身 case id 执行只读 resume;只有首页或未绑定会话才按账户当前 case 复用或创建;历史终态不再覆盖另一个未完成 case,终态后账户刷新接受服务端最新投影。
|
||||
- 验证:入口契约测试锁定历史会话按自身 case 恢复、首页重新校正仍创建独立会话,以及历史终态不覆盖当前未完成 case;发布后用 production 合成账号复查范围终态、刷新恢复、首页入口与余额不变量。
|
||||
- 防复发:聊天会话的 `rectificationCaseId` 是历史查看的第一绑定键;账户 `rectificationCase` 只代表当前未完成案例,二者不得相互替代。
|
||||
- 相关记录:BUG-017、BUG-019、BUG-020
|
||||
- 复发自:无
|
||||
- 修复版本:待提交(production gate)
|
||||
|
||||
+38
-20
@@ -1913,18 +1913,27 @@ export default function Home() {
|
||||
rectificationCase: account.rectificationCase,
|
||||
hasConfirmedBirthTime: account.hasConfirmedBirthTime,
|
||||
});
|
||||
const resumableSession = action === "resume" && account.rectificationCase
|
||||
const sourceBoundCaseId = sourceSession.sessionType === "birth_time_rectification"
|
||||
? sourceSession.rectificationCaseId
|
||||
: null;
|
||||
const accountResumeCase = action === "resume" ? account.rectificationCase : null;
|
||||
const resumeTarget = sourceBoundCaseId
|
||||
? {
|
||||
caseId: sourceBoundCaseId,
|
||||
turnVersion: accountResumeCase?.caseId === sourceBoundCaseId
|
||||
? accountResumeCase.turnVersion
|
||||
: 0,
|
||||
}
|
||||
: accountResumeCase;
|
||||
const resumableSession = !sourceBoundCaseId && accountResumeCase
|
||||
? sessions.find((session) => session.sessionType === "birth_time_rectification"
|
||||
&& session.rectificationCaseId === account.rectificationCase?.caseId)
|
||||
&& session.rectificationCaseId === accountResumeCase.caseId)
|
||||
?? sessions.find((session) => session.sessionType === "birth_time_rectification"
|
||||
&& session.rectificationCaseId === null)
|
||||
?? null
|
||||
: null;
|
||||
const canReuseSourceRectificationSession = action === "resume"
|
||||
&& sourceSession.sessionType === "birth_time_rectification"
|
||||
&& account.rectificationCase !== null
|
||||
&& (sourceSession.rectificationCaseId === account.rectificationCase.caseId
|
||||
|| sourceSession.rectificationCaseId === null);
|
||||
const canReuseSourceRectificationSession = sourceSession.sessionType === "birth_time_rectification"
|
||||
&& (sourceBoundCaseId !== null || accountResumeCase !== null);
|
||||
const rectificationSession = canReuseSourceRectificationSession
|
||||
? sourceSession
|
||||
: resumableSession ?? createSession(modelCatalog.defaultModelId, "birth_time_rectification");
|
||||
@@ -1957,7 +1966,7 @@ export default function Home() {
|
||||
setActiveSessionId(rectificationSession.id);
|
||||
try {
|
||||
let turn: ConversationalRectificationTurn;
|
||||
if (action !== "resume" || !account.rectificationCase) {
|
||||
if (!resumeTarget) {
|
||||
const durable = await durableRectificationQuestionHandoff.current.load();
|
||||
turn = durable && durable.status !== "consumed"
|
||||
? durable.turn
|
||||
@@ -1967,8 +1976,10 @@ export default function Home() {
|
||||
pendingConsultationQuestion: requestedQuestion,
|
||||
});
|
||||
} else {
|
||||
let current = account.rectificationCase;
|
||||
if (pendingConsultationQuestion) {
|
||||
let current = resumeTarget;
|
||||
const canAttachQuestion = pendingConsultationQuestion
|
||||
&& accountResumeCase?.caseId === current.caseId;
|
||||
if (canAttachQuestion) {
|
||||
try {
|
||||
turn = await durableRectificationQuestionHandoff.current.attach({
|
||||
caseId: current.caseId,
|
||||
@@ -2063,16 +2074,22 @@ export default function Home() {
|
||||
...current,
|
||||
hasConfirmedBirthTime: current.hasConfirmedBirthTime
|
||||
|| (turn.status === "completed" && turn.candidate.status === "confirmed"),
|
||||
rectificationCase: {
|
||||
caseId: turn.caseId,
|
||||
journeyProtocol: "conversational-evidence-v3",
|
||||
status: turn.status,
|
||||
turnVersion: turn.turnVersion,
|
||||
isRevision: current.rectificationCase?.isRevision
|
||||
?? current.hasConfirmedBirthTime,
|
||||
preservesActiveTime: current.rectificationCase?.preservesActiveTime
|
||||
?? current.hasConfirmedBirthTime,
|
||||
},
|
||||
rectificationCase: ["active", "paused", "confirming"].includes(turn.status)
|
||||
? {
|
||||
caseId: turn.caseId,
|
||||
journeyProtocol: "conversational-evidence-v3",
|
||||
status: turn.status,
|
||||
turnVersion: turn.turnVersion,
|
||||
isRevision: current.rectificationCase?.caseId === turn.caseId
|
||||
? current.rectificationCase.isRevision
|
||||
: current.hasConfirmedBirthTime,
|
||||
preservesActiveTime: current.rectificationCase?.caseId === turn.caseId
|
||||
? current.rectificationCase.preservesActiveTime
|
||||
: current.hasConfirmedBirthTime,
|
||||
}
|
||||
: current.rectificationCase?.caseId === turn.caseId
|
||||
? null
|
||||
: current.rectificationCase,
|
||||
} : current);
|
||||
if (turn.status === "completed"
|
||||
&& turn.candidate.status === "confirmed"
|
||||
@@ -2094,6 +2111,7 @@ export default function Home() {
|
||||
.then((latest) => {
|
||||
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
|
||||
setAccount((current) => {
|
||||
if (turn.status === "completed" || turn.status === "abandoned") return latest;
|
||||
if (latest.rectificationCase?.caseId !== turn.caseId
|
||||
|| latest.rectificationCase.turnVersion < turn.turnVersion) return current;
|
||||
return latest;
|
||||
|
||||
@@ -149,21 +149,33 @@ test("homepage reuses the session bound to an unfinished rectification case", ()
|
||||
const end = source.indexOf("function handleConversationalRectificationTurn", start);
|
||||
const handler = source.slice(start, end);
|
||||
|
||||
assert.match(handler, /action === "resume" && account\.rectificationCase/);
|
||||
assert.match(handler, /session\.rectificationCaseId === account\.rectificationCase\?\.caseId/);
|
||||
assert.match(handler, /const accountResumeCase = action === "resume" \? account\.rectificationCase : null/);
|
||||
assert.match(handler, /session\.rectificationCaseId === accountResumeCase\.caseId/);
|
||||
assert.match(handler, /resumableSession \?\? createSession/);
|
||||
});
|
||||
|
||||
test("starting again after a completed rectification creates a new dedicated session", () => {
|
||||
test("a bound rectification session resumes its own case while a homepage restart stays dedicated", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const start = source.indexOf("async function openBirthTimeRectification");
|
||||
const end = source.indexOf("function handleConversationalRectificationTurn", start);
|
||||
const handler = source.slice(start, end);
|
||||
|
||||
assert.match(handler, /const canReuseSourceRectificationSession = action === "resume"/);
|
||||
assert.match(handler, /sourceSession\.rectificationCaseId === account\.rectificationCase\.caseId/);
|
||||
assert.match(handler, /const sourceBoundCaseId = sourceSession\.sessionType === "birth_time_rectification"/);
|
||||
assert.match(handler, /const resumeTarget = sourceBoundCaseId/);
|
||||
assert.match(handler, /caseId: sourceBoundCaseId/);
|
||||
assert.match(handler, /if \(!resumeTarget\) \{[\s\S]*?type: "start"/);
|
||||
assert.match(handler, /const rectificationSession = canReuseSourceRectificationSession[\s\S]*?: resumableSession \?\? createSession/);
|
||||
assert.doesNotMatch(handler, /const rectificationSession = sourceSession\.sessionType === "birth_time_rectification"/);
|
||||
assert.match(handler, /type: "resume",[\s\S]*?caseId: current\.caseId/);
|
||||
});
|
||||
|
||||
test("historical completed rectification does not replace the account's unfinished case", () => {
|
||||
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const start = source.indexOf("function handleConversationalRectificationTurn");
|
||||
const end = source.indexOf("async function draftSynastryQuestionFromChart", start);
|
||||
const handler = source.slice(start, end);
|
||||
|
||||
assert.match(handler, /current\.rectificationCase\?\.caseId === turn\.caseId[\s\S]*?\? null[\s\S]*?: current\.rectificationCase/);
|
||||
assert.match(handler, /turn\.status === "completed" \|\| turn\.status === "abandoned"\) return latest/);
|
||||
});
|
||||
|
||||
test("rectify-first suggestions hand the source question to a dedicated rectification session", () => {
|
||||
|
||||
Reference in New Issue
Block a user