124 lines
7.7 KiB
TypeScript
124 lines
7.7 KiB
TypeScript
// 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 type { Locator } from "@playwright/test";
|
|
import { expect, gotoWorkbench, saveScreenshot, test } from "../fixtures/test";
|
|
import { selectors, sessionTab } from "../fixtures/selectors";
|
|
|
|
test.describe("projection diagnostics visibility", () => {
|
|
test.use({ scenarioId: "projection-degraded-diagnostics" });
|
|
|
|
test("turn and trace projection blockers are visible without changing lifecycle", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page, "/workbench/sessions/ses_projection_degraded");
|
|
await expect(page.locator(sessionTab("ses_projection_degraded"))).toHaveAttribute("data-status", "running");
|
|
await expect(page.locator(sessionTab("ses_projection_degraded"))).toHaveAttribute("data-projection-health", "degraded");
|
|
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`).last();
|
|
await expect(card).toContainText("状态更新超时");
|
|
const diagnostic = card.locator(".projection-diagnostic");
|
|
await expectCompactDiagnostic(diagnostic, card, "trc_projection_degraded");
|
|
await expect(diagnostic).not.toContainText("valuesPrinted");
|
|
await expandDiagnostic(diagnostic);
|
|
await expect(diagnostic).toContainText("agentrun_result_timeout");
|
|
await expect(diagnostic).toContainText("trace_id");
|
|
await expect(diagnostic).toContainText("trc_projection_degraded");
|
|
await expect(diagnostic).toContainText("valuesPrinted");
|
|
await expect(diagnostic).toContainText("false");
|
|
await expect(card.locator(selectors.traceTimeline)).toHaveAttribute("data-projection-health", "degraded");
|
|
await expect(card.locator(".trace-empty")).toContainText("状态更新超时");
|
|
await expect(page.locator(selectors.commandSend)).toHaveAttribute("data-action", "cancel");
|
|
await saveScreenshot(page, testInfo, "projection-degraded-visible");
|
|
});
|
|
});
|
|
|
|
test.describe("SSE projection errors", () => {
|
|
test.use({ scenarioId: "projection-sse-error" });
|
|
|
|
test("workbench.error binds to the current message diagnostic", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page, "/workbench/sessions/ses_projection_sse_error");
|
|
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`).last();
|
|
await expect(card).toContainText("SSE 投影事件异常");
|
|
const diagnostic = card.locator(".projection-diagnostic");
|
|
await expectCompactDiagnostic(diagnostic, card, "trc_projection_sse_error");
|
|
await expect(diagnostic).not.toContainText("req_e2e_workbench_sse_projection_error");
|
|
await expandDiagnostic(diagnostic);
|
|
await expect(diagnostic).toContainText("workbench_sse_projection_error");
|
|
await expect(diagnostic).toContainText("req_e2e_workbench_sse_projection_error");
|
|
await expect(diagnostic).toContainText("/v1/workbench/events");
|
|
await expect(page.locator(sessionTab("ses_projection_sse_error"))).toHaveAttribute("data-projection-health", "degraded");
|
|
await saveScreenshot(page, testInfo, "projection-sse-error-visible");
|
|
});
|
|
});
|
|
|
|
test.describe("REST gap hydration errors", () => {
|
|
test.use({ scenarioId: "trace-hydration-timeout-diagnostic" });
|
|
|
|
test("trace hydration timeout is visible without terminal inference", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page, "/workbench/sessions/ses_trace_hydration_timeout");
|
|
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="running"]`).last();
|
|
await expect(card).toContainText("Trace 更新超时");
|
|
const diagnostic = card.locator(".projection-diagnostic");
|
|
await expectCompactDiagnostic(diagnostic, card, "33333333333333333333333333333333");
|
|
await expect(diagnostic).not.toContainText("req_e2e_trace_hydration_timeout");
|
|
await expect(diagnostic).not.toContainText("/v1/workbench/traces/trc_trace_hydration_timeout/events");
|
|
await expandDiagnostic(diagnostic);
|
|
await expect(diagnostic).toContainText("trace_hydration_timeout");
|
|
await expect(diagnostic).toContainText("33333333333333333333333333333333");
|
|
await expect(diagnostic).toContainText("req_e2e_trace_hydration_timeout");
|
|
await expect(diagnostic).toContainText("/v1/workbench/traces/trc_trace_hydration_timeout/events");
|
|
await expect(card).toHaveAttribute("data-status", "running");
|
|
await expect(card.locator(selectors.traceTimeline)).toHaveAttribute("data-projection-health", "degraded");
|
|
await expect(page.locator(selectors.commandSend)).toHaveAttribute("data-action", "cancel");
|
|
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 without surfacing transient hydration diagnostics", 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();
|
|
const finalResponse = card.locator(".message-text").filter({ hasText: "已完成的 sealed final response 不应被诊断覆盖。" });
|
|
await expect(finalResponse).toBeVisible();
|
|
await expect(finalResponse).not.toContainText("Trace 更新超时");
|
|
await expect(card.locator(".projection-diagnostic")).toHaveCount(0);
|
|
await saveScreenshot(page, testInfo, "sealed-final-response-diagnostic-separate");
|
|
});
|
|
});
|
|
|
|
async function expectCompactDiagnostic(diagnostic: Locator, container: Locator, traceId: string): Promise<void> {
|
|
await expect(diagnostic).toContainText(`trace_id=${traceId}`);
|
|
const toggle = diagnostic.getByRole("button", { name: "诊断详情" });
|
|
await expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
await expect(toggle).toBeVisible();
|
|
const diagnosticBox = await diagnostic.boundingBox();
|
|
const containerBox = await container.boundingBox();
|
|
expect(diagnosticBox?.width ?? 0).toBeGreaterThan(0);
|
|
if (containerBox && diagnosticBox) expect(diagnosticBox.width).toBeLessThan(containerBox.width - 12);
|
|
}
|
|
|
|
async function expandDiagnostic(diagnostic: Locator): Promise<void> {
|
|
const toggle = diagnostic.getByRole("button", { name: "诊断详情" });
|
|
await toggle.click();
|
|
await expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
}
|
|
|
|
test.describe("trace detail final response separation", () => {
|
|
test.use({ scenarioId: "trace-final-response-not-message-authority" });
|
|
|
|
test("keeps trace finalResponse out of the main message projection", async ({ page }, testInfo) => {
|
|
await gotoWorkbench(page, "/workbench/sessions/ses_trace_final_conflict");
|
|
const card = page.locator(`${selectors.messageCard}[data-role="agent"][data-status="completed"]`).last();
|
|
const messageText = card.locator(".message-markdown.message-text");
|
|
await expect(messageText).toContainText("messageProjection 的 sealed final response 才是主消息正文。");
|
|
const traceResponse = await page.request.get("/v1/workbench/traces/trc_trace_final_conflict/events?limit=20");
|
|
expect(traceResponse.status()).toBe(200);
|
|
const tracePayload = await traceResponse.json();
|
|
expect(tracePayload.finalResponse.text).toBe("TRACE_DETAIL_CONFLICT_SHOULD_NOT_REPLACE_MESSAGE");
|
|
await page.waitForTimeout(500);
|
|
await expect(messageText).toContainText("messageProjection 的 sealed final response 才是主消息正文。");
|
|
await expect(messageText).not.toContainText("TRACE_DETAIL_CONFLICT_SHOULD_NOT_REPLACE_MESSAGE");
|
|
await saveScreenshot(page, testInfo, "trace-final-response-not-message-authority");
|
|
});
|
|
});
|