fix: separate sealed final response diagnostics

This commit is contained in:
lyon
2026-06-19 21:22:21 +08:00
parent 8b5cefd1b7
commit fa924a64c8
3 changed files with 68 additions and 5 deletions
@@ -1,4 +1,4 @@
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-19-p0-projector-resume; PJ2026-010403 API契约 draft-2026-06-18-r1.
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-19-p2-terminal-sealed-final-response; PJ2026-010403 API契约 draft-2026-06-18-r1.
// Responsibility: Same-origin fake Workbench API and static server for Playwright browser regression tests.
import { createReadStream, existsSync, statSync } from "node:fs";
@@ -139,6 +139,7 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
const traceId = decodeURIComponent(traceMatch[1] ?? "");
if (state.scenarioId === "completed-replay-detail-404" && traceId === "trc_completed") return json(response, 404, { ok: false, status: 404, error: { code: "trace_replay_unavailable" } });
if (state.scenarioId === "trace-hydration-timeout-diagnostic" && traceId === "trc_trace_hydration_timeout") return json(response, 504, { ok: false, status: 504, error: { code: "trace_hydration_timeout", message: "Trace 更新超时,运行记录暂不可见。" } });
if (state.scenarioId === "sealed-final-response-diagnostics" && traceId === "trc_sealed_final_diag") return json(response, 504, { ok: false, status: 504, error: { code: "trace_hydration_timeout", message: "Trace 更新超时,运行记录暂不可见。" } });
if (traceId === state.staleTraceId) return json(response, 502, { ok: false, status: 502, error: { code: "upstream_unavailable", message: "stale trace is unavailable" } });
return json(response, 200, workbenchTracePayload(traceId, url));
}
@@ -386,6 +387,10 @@ function createScenarioState(scenarioId: string): ScenarioState {
sessions.unshift(traceHydrationTimeoutSession());
traces.trc_trace_hydration_timeout = traceHydrationTimeoutTrace();
}
if (id === "sealed-final-response-diagnostics") {
sessions.unshift(sealedFinalResponseDiagnosticSession());
traces.trc_sealed_final_diag = sealedFinalResponseDiagnosticTrace();
}
if (id === "scroll-follow-long-trace") {
const session = scrollFollowSession();
sessions.unshift(session);
@@ -425,6 +430,8 @@ function createScenarioState(scenarioId: string): ScenarioState {
? "ses_projection_sse_error"
: id === "trace-hydration-timeout-diagnostic"
? "ses_trace_hydration_timeout"
: id === "sealed-final-response-diagnostics"
? "ses_sealed_final_diag"
: id === "create-while-route-loading"
? "ses_running"
: base.selectedSessionId;
@@ -706,6 +713,45 @@ function traceHydrationTimeoutSession(): SessionRecord {
};
}
function sealedFinalResponseDiagnosticTrace(): JsonRecord {
const now = new Date().toISOString();
const finalText = "已完成的 sealed final response 不应被诊断覆盖。";
return {
traceId: "trc_sealed_final_diag",
status: "completed",
sessionId: "ses_sealed_final_diag",
threadId: "thr_sealed_final_diag",
turnId: "turn_sealed_final_diag",
events: [],
eventCount: 1,
fullTraceLoaded: false,
hasMore: true,
finalResponse: { text: finalText, status: "completed" },
terminalEvidence: { terminal: true, status: "completed" },
sealedAt: now
};
}
function sealedFinalResponseDiagnosticSession(): SessionRecord {
const now = new Date().toISOString();
const finalText = "已完成的 sealed final response 不应被诊断覆盖。";
const trace = sealedFinalResponseDiagnosticTrace();
return {
sessionId: "ses_sealed_final_diag",
threadId: "thr_sealed_final_diag",
status: "completed",
lastTraceId: "trc_sealed_final_diag",
updatedAt: now,
messageCount: 2,
firstUserMessagePreview: "sealed final response diagnostic separation",
turnSummary: { traceId: "trc_sealed_final_diag", status: "completed", running: false, terminal: true, sealedAt: now },
messages: [
{ id: "msg_sealed_final_user", messageId: "msg_sealed_final_user", role: "user", title: "用户", text: "sealed final response diagnostic separation", status: "sent", createdAt: now, sessionId: "ses_sealed_final_diag", threadId: "thr_sealed_final_diag", turnId: "turn_sealed_final_diag" },
{ id: "msg_sealed_final_agent", messageId: "msg_sealed_final_agent", role: "agent", title: "Code Agent", text: finalText, status: "completed", createdAt: now, updatedAt: now, sessionId: "ses_sealed_final_diag", threadId: "thr_sealed_final_diag", traceId: "trc_sealed_final_diag", turnId: "turn_sealed_final_diag", sealedAt: now, runnerTrace: trace, finalResponse: { text: finalText, status: "completed" } }
]
};
}
function noFinalRunningTrace(): JsonRecord {
const createdAt = new Date().toISOString();
return {
@@ -1,6 +1,6 @@
<script setup lang="ts">
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
// Confirms Workbench message visibility after Vue render and browser paint.
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-0104010803 draft-2026-06-19-p2-terminal-sealed-final-response.
// Confirms Workbench message visibility after Vue render and keeps sealed final responses separate from diagnostics.
import { computed, nextTick, onMounted, ref, watch } from "vue";
import type { ChatMessage } from "@/types";
@@ -80,7 +80,7 @@ function isAwaitingAgentBody(message: ChatMessage): boolean {
}
function showMessageText(message: ChatMessage): boolean {
return !messageDiagnosticText(message) && Boolean(visibleMessageText(message));
return Boolean(visibleMessageText(message));
}
function messageDiagnosticText(message: ChatMessage): string | null {
@@ -132,8 +132,8 @@ function traceAutoExpanded(message: ChatMessage): boolean | null {
</header>
<TraceTimeline v-if="message.role === 'agent' && message.runnerTrace" :trace="message.runnerTrace" :auto-expanded="traceAutoExpanded(message)" :storage-key="traceStorageKey(message)" />
<LoadingState v-if="isAwaitingAgentBody(message)" class="message-loading" label="思考中..." compact />
<MessageMarkdown v-if="showMessageText(message)" class="message-text" :source="visibleMessageText(message)" />
<p v-if="messageDiagnosticText(message)" class="message-text projection-diagnostic">{{ messageDiagnosticText(message) }}</p>
<MessageMarkdown v-else-if="showMessageText(message)" class="message-text" :source="visibleMessageText(message)" />
</article>
<div v-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0 && workbench.error" class="conversation-empty-hint conversation-error-hint">加载失败{{ workbench.error }}</div>
<div v-else-if="!workbench.sessionDetailLoading && workbench.activeMessages.length === 0" class="conversation-empty-hint">发起对话或从左侧选择 session</div>
@@ -1,3 +1,6 @@
// SPEC: PJ2026-010401 Web工作台 draft-2026-06-18-r1; PJ2026-0104010803 唯一投影 draft-2026-06-19-p2-terminal-sealed-final-response.
// Responsibility: Workbench diagnostic visibility regression, including separation from sealed final response.
import { expect, gotoWorkbench, saveScreenshot, test } from "../fixtures/test";
import { selectors, sessionTab } from "../fixtures/selectors";
@@ -42,3 +45,17 @@ test.describe("REST gap hydration errors", () => {
await saveScreenshot(page, testInfo, "trace-hydration-timeout-visible");
});
});
test.describe("sealed final response diagnostics separation", () => {
test.use({ scenarioId: "sealed-final-response-diagnostics" });
test("keeps completed final response visible while showing diagnostics separately", async ({ page }, testInfo) => {
await gotoWorkbench(page, "/workbench/sessions/ses_sealed_final_diag");
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`).last();
await expect(card.locator(".projection-diagnostic")).toContainText("Trace 更新超时");
const finalResponse = card.locator(".message-text").filter({ hasText: "已完成的 sealed final response 不应被诊断覆盖。" });
await expect(finalResponse).toBeVisible();
await expect(finalResponse).not.toContainText("Trace 更新超时");
await saveScreenshot(page, testInfo, "sealed-final-response-diagnostic-separate");
});
});