fix(rectification): hydrate persisted case turns
Staging Backend Quality Gate / validate (pull_request) Successful in 15m6s
Staging Backend Quality Gate / publish (pull_request) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-12 10:45:49 +08:00
parent 31fa6aaa50
commit 220828d6f0
5 changed files with 50 additions and 23 deletions
+2 -1
View File
@@ -2372,6 +2372,7 @@ export default function Home() {
status: String((turn.receipt as { status?: unknown }).status ?? ""),
phases: Array.isArray((turn.receipt as { phases?: unknown }).phases) ? (turn.receipt as { phases: unknown[] }).phases.map(String) : [],
tools: Array.isArray((turn.receipt as { tools?: unknown }).tools) ? (turn.receipt as { tools: unknown[] }).tools.map(String) : [],
methods: Array.isArray((turn.receipt as { methods?: unknown }).methods) ? (turn.receipt as { methods: unknown[] }).methods.map(String) : [],
skill_name: typeof (turn.receipt as { skill_name?: unknown }).skill_name === "string" ? (turn.receipt as { skill_name: string }).skill_name : undefined,
skill_version: typeof (turn.receipt as { skill_version?: unknown }).skill_version === "string" ? (turn.receipt as { skill_version: string }).skill_version : undefined,
} : null,
@@ -3480,7 +3481,7 @@ export default function Home() {
{rectificationSurfaceOpen && rectificationCaseId && (
<ConversationalBirthTimeRectification
key={`${rectificationSessionId}-${rectificationCaseId}`}
key={`${rectificationSessionId}-${rectificationCaseId}-${rectificationTurns.at(-1)?.id ?? "loading"}`}
caseId={rectificationCaseId}
sessionId={rectificationSessionId ?? ""}
readonly={rectificationReadonly}
@@ -13,6 +13,7 @@ export type PersistedRectificationTurn = Readonly<{
status: string;
phases: readonly string[];
tools: readonly string[];
methods?: readonly string[];
skill_name?: string;
skill_version?: string;
}> | null;
@@ -112,6 +112,28 @@ function activityFromReceipt(receipt: PersistedTurn["receipt"]): ReceiptActivity
};
}
function messagesFromTurns(initialTurns: readonly PersistedTurn[]): RenderMessage[] {
return initialTurns.flatMap((turn, index): RenderMessage[] => {
const key = `persisted-${turn.id}-${index}`;
if (turn.role === "assistant") {
return [{
role: "assistant",
text: turn.text ?? "",
renderKey: key,
state: turn.status === "completed" ? "settled" : "thinking",
receiptActivity: activityFromReceipt(turn.receipt),
receiptStatus: turn.receipt?.status,
}];
}
return [{
role: "user",
text: turn.text ?? "",
renderKey: key,
state: "settled",
}];
});
}
export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const {
caseId,
@@ -131,27 +153,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onRestart,
} = props;
const [messages, setMessages] = useState<RenderMessage[]>(() =>
initialTurns.flatMap((turn, index): RenderMessage[] => {
const key = `persisted-${turn.id}-${index}`;
if (turn.role === "assistant") {
return [{
role: "assistant",
text: turn.text ?? "",
renderKey: key,
state: turn.status === "completed" ? "settled" : "thinking",
receiptActivity: activityFromReceipt(turn.receipt),
receiptStatus: turn.receipt?.status,
}];
}
return [{
role: "user",
text: turn.text ?? "",
renderKey: key,
state: "settled",
}];
}),
);
const [messages, setMessages] = useState<RenderMessage[]>(() => messagesFromTurns(initialTurns));
const [draft, setDraft] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
@@ -29,6 +29,14 @@ test("birth-time rectification entry mounts the V9 case-ref chat", () => {
assert.match(component, /initialTurns:/);
});
test("persisted rectification turns hydrate after the async Case refresh", () => {
assert.match(chat, /function messagesFromTurns\(initialTurns:/);
assert.match(chat, /useState<RenderMessage\[\]>\(\(\) => messagesFromTurns\(initialTurns\)\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(page, /methods: Array\.isArray\(\(turn\.receipt as \{ methods\?: unknown \}\)\.methods\)/);
assert.match(component, /methods\?: readonly string\[\]/);
});
test("opening is server-owned: shouldStartOpening drives the first turn, never client history", () => {
assert.doesNotMatch(chat, /initialMessages\.length > 0 \|\| openingStarted/);
assert.doesNotMatch(chat, /agenticOpeningInstruction|用户刚进入生时校正会话/);
@@ -100,7 +108,7 @@ test("agent tool calls never end silently; the runner owns completion and failur
test("persisted turns survive remounts; duplicate openings are suppressed by the server", () => {
assert.match(chat, /initialTurns/);
assert.match(chat, /const openingStarted = useRef\(false\)/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}`\}/);
assert.match(page, /key=\{`\$\{rectificationSessionId\}-\$\{rectificationCaseId\}-\$\{rectificationTurns\.at\(-1\)\?\.id \?\? "loading"\}`\}/);
assert.match(page, /initialTurns=\{rectificationTurns\}/);
assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/);
});