diff --git a/web/hwlab-cloud-web/scripts/workbench-message-projection-runtime.test.ts b/web/hwlab-cloud-web/scripts/workbench-message-projection-runtime.test.ts index dc7a787b..e905bc97 100644 --- a/web/hwlab-cloud-web/scripts/workbench-message-projection-runtime.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-message-projection-runtime.test.ts @@ -5,7 +5,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import type { AgentChatResultResponse, ChatMessage } from "../src/types/index.ts"; -import { clearRunnerTraceTransientDiagnostics, mergeTerminalResultTrace, messageStatusPatchForTerminalMerge, messageTimingPatchForMerge, nonBlockingProjection, normalizeAgentError, projectionFromResult, shouldClearCompletedTurnDiagnostics, terminalMessagePatchFromTurnResult, terminalMessageTimingPatchForNormalize } from "../src/stores/workbench-message-projection-runtime.ts"; +import { clearRunnerTraceTransientDiagnostics, mergeTerminalResultTrace, messageStatusPatchForTerminalMerge, messageTimingPatchForMerge, messageTimingSealPatchForProjectionMerge, nonBlockingProjection, normalizeAgentError, projectionFromResult, shouldClearCompletedTurnDiagnostics, terminalMessagePatchFromTurnResult, terminalMessageTimingPatchForNormalize } from "../src/stores/workbench-message-projection-runtime.ts"; test("terminal result merge preserves runner trace evidence and timing", () => { const previous: ChatMessage["runnerTrace"] = { @@ -199,3 +199,29 @@ test("terminal seal blocks stale running snapshots after session restore", () => assert.equal(terminal?.durationMs, 4000); assert.equal(terminal?.error?.message, "provider stream disconnected"); }); + +test("terminal projection page seal preserves final response object", () => { + const previous = { + role: "agent", + status: "completed", + text: "final answer", + finalResponse: { text: "final answer", status: "completed", sealed: true }, + traceAutoLifecycle: "terminal", + timing: { startedAt: "2026-01-01T00:00:00.000Z", finishedAt: "2026-01-01T00:00:05.000Z", durationMs: 5000, valuesRedacted: true } + } as ChatMessage; + const incoming = { + role: "agent", + status: "running", + text: "", + traceAutoLifecycle: "running", + timing: { startedAt: "2026-01-01T00:00:00.000Z", valuesRedacted: true } + } as ChatMessage; + + const patch = messageTimingSealPatchForProjectionMerge(incoming, previous); + + assert.equal(patch.status, "completed"); + assert.equal(patch.traceAutoLifecycle, "terminal"); + assert.equal(patch.text, "final answer"); + assert.deepEqual(patch.finalResponse, { text: "final answer", status: "completed", sealed: true }); + assert.equal(patch.durationMs, 5000); +}); diff --git a/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.ts b/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.ts index fb70db95..5f5cfd82 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.ts @@ -217,6 +217,8 @@ function messageTerminalSealPatchForProjectionMerge(message: ChatMessage, previo if (!previous || !isTerminalMessageStatus(previous.status)) return incomingTimingPatch; const patch: Partial = { status: previous.status, traceAutoLifecycle: previous.traceAutoLifecycle ?? "terminal" }; if (typeof previous.text === "string" && previous.text.trim()) patch.text = previous.text; + const finalResponse = (previous as Record).finalResponse; + if (finalResponse) patch.finalResponse = finalResponse; const previousTiming = normalizeTimingProjection(previous); if (firstPositiveFiniteNumber(previousTiming?.durationMs) === null) return { ...terminalMessageTimingPatchForNormalize(message), ...patch }; return { ...messageTimingPatch(previous), ...patch };