import { expect, test, type Page } from "@playwright/test"; import { buildE2EAuthHeaders } from "./helpers/auth"; import { connectedUser, fulfillJson } from "./helpers/flows-pane-runs-fixtures"; test("recent runs open a detailed observability dialog", async ({ page }) => { await checkRunDialog(page, false); }); test("draft snapshots are identified in the run dialog", async ({ page }) => { await checkRunDialog(page, true); }); async function checkRunDialog(page: Page, draftTest: boolean) { await page.route("**/api/realtime/events?*", (route) => route.fulfill({ status: 204 }) ); // Width must be >= 1520 so the flows container (minus ~240px sidebar) exceeds // the 1280px dock-mode threshold. await page.setViewportSize({ width: 1600, height: 900 }); await page.context().setExtraHTTPHeaders({ ...buildE2EAuthHeaders(connectedUser.id), "x-mogplex-scope-kind": "personal", "x-mogplex-scope-slug": connectedUser.username, "x-mogplex-scope-id": connectedUser.id, }); await page.addInitScript(() => { window.localStorage.setItem("theme", "dark"); window.localStorage.setItem( "flow-assistant-panel", JSON.stringify({ state: { open: true, }, version: 0, }) ); }); const flowPayload = { id: "flow-1", installation_id: 101, name: "NEXTJS-REVIEWER · PR opened", description: "Migrated from Trigger", notes: "Capture intent, guardrails, and context for this flow.", source_kind: "github", status: "active", last_run_status: "success", published_version_id: "version-1", published_version: { id: "version-1", flow_id: "flow-1", version_number: 1, graph: { nodes: [ { id: "start", type: "start", position: { x: 120, y: 160 }, data: { label: "PR opened", event: "pr_opened" }, }, { id: "agent-1", type: "agent", position: { x: 380, y: 160 }, data: { label: "NEXTJS-REVIEWER", agentId: "agent-1" }, }, { id: "end", type: "end", position: { x: 660, y: 160 }, data: { label: "Done" }, }, ], edges: [ { id: "edge-1", source: "start", target: "agent-1" }, { id: "edge-2", source: "agent-1", target: "end" }, ], viewport: { x: 0, y: 0, zoom: 1 }, }, created_at: "2026-03-28T17:00:00.000Z", }, draft_graph: { nodes: [ { id: "start", type: "start", position: { x: 120, y: 160 }, data: { label: "PR opened", event: "pr_opened" }, }, { id: "agent-1", type: "agent", position: { x: 380, y: 160 }, data: { label: "NEXTJS-REVIEWER", agentId: "agent-1" }, }, { id: "end", type: "end", position: { x: 660, y: 160 }, data: { label: "Done" }, }, ], edges: [ { id: "edge-1", source: "start", target: "agent-1" }, { id: "edge-2", source: "agent-1", target: "end" }, ], viewport: { x: 0, y: 0, zoom: 1 }, }, }; const runSummary = { id: "job-1", assignment_id: null, trigger_id: null, flow_id: "flow-1", flow_version_id: "version-1", runtime_provider: "trigger", runtime_run_id: "run_123", workflow_run_id: null, retry_of_job_run_id: null, status: "failed", created_at: "2026-03-28T11:00:00.000Z", started_at: "2026-03-28T11:01:00.000Z", completed_at: "2026-03-28T11:02:00.000Z", input_tokens: 120, output_tokens: 80, duration_ms: 60000, error: "Lint failed on CI", start_attempts: 2, last_start_attempt_at: "2026-03-28T11:01:00.000Z", last_start_error: "Lint failed on CI", last_start_source: "webhook", cancel_requested_at: null, cancelled_at: null, cancel_reason: null, cancel_error: null, metadata: { repo_id: "repo-1", repo_full_name: "webrenew/blackbox", source_type: "pr_opened", pr_number: 42, pr_url: "https://github.com/webrenew/blackbox/pull/42", }, source_kind: "flow", source_type: "pr_opened", repo: { id: "repo-1", full_name: "webrenew/blackbox", }, agent: { id: null, name: null, slug: null, }, latest_ai_call: { id: "call-1", status: "success", model: "minimax/minimax-m2.5", total_tokens: 200, tool_calls_count: 1, started_at: "2026-03-28T11:01:05.000Z", }, cancelable: false, repairable: false, requeueable: true, latest_dispatch_event: { outcome: "start_failed", reason: "LINT_FAILED", created_at: "2026-03-28T11:01:30.000Z", }, node_runs: [ { id: "node-run-1", user_id: "user-1", job_run_id: "job-1", flow_id: "flow-1", flow_version_id: "version-1", node_id: "agent-1", node_type: "agent", node_label: "NEXTJS-REVIEWER", status: "failed", started_at: "2026-03-28T11:01:00.000Z", completed_at: "2026-03-28T11:02:00.000Z", duration_ms: 60000, error: "Lint failed on CI", output: { role: "review", text: "Reported lint failures and suggested the exact fix path.", }, created_at: "2026-03-28T11:01:00.000Z", }, ], }; const editPatch = [ "diff --git a/src/widget.ts b/src/widget.ts", "index 1111111..2222222 100644", "--- a/src/widget.ts", "+++ b/src/widget.ts", "@@ -1 +1 @@", "-export const widget = 'old'", "+export const widget = 'new'", ].join("\n"); const runDetail = { ...runSummary, metadata: { ...runSummary.metadata, flow_version_number: 1, automation_test: draftTest, flow_version_is_test: draftTest, working_branch: "mogplex/automation-0123456789abcdef", input: { slug: "acme-plumbing" }, trigger: { credential: "integration", key_id: "key-1", label: "Webrenew agent", }, }, node_runs: [ ...runSummary.node_runs, { id: "node-run-2", user_id: "user-1", job_run_id: "job-1", flow_id: "flow-1", flow_version_id: "version-1", node_id: "agent-2", node_type: "agent", node_label: "PR-FIXER", status: "success", started_at: "2026-03-28T11:02:00.000Z", completed_at: "2026-03-28T11:02:30.000Z", duration_ms: 30000, error: null, output: { role: "edit", text: "Applied a focused widget fix.", tool_calls: [ { name: "updateFile", input: { path: "src/widget.ts" }, output: { success: true, path: "src/widget.ts", branch: "fix/widget", commitSha: "abcdef1234567890", commitUrl: "https://github.com/webrenew/blackbox/commit/abcdef1234567890", patch: editPatch, }, }, ], }, created_at: "2026-03-28T11:02:00.000Z", }, ], dispatch_events: [ { id: "dispatch-1", event_kind: "enqueue", outcome: "queued", reason: null, metadata: { source: "webhook" }, created_at: "2026-03-28T11:00:30.000Z", }, { id: "dispatch-2", event_kind: "start", outcome: "start_failed", reason: "LINT_FAILED", metadata: { retryable: true }, created_at: "2026-03-28T11:01:30.000Z", }, ], ai_calls: [ { id: "call-1", user_id: "user-1", type: "agent", model: "minimax/minimax-m2.5", input_tokens: 120, output_tokens: 80, total_tokens: 200, duration_ms: 45000, started_at: "2026-03-28T11:01:05.000Z", completed_at: "2026-03-28T11:01:50.000Z", status: "success", error: null, conversation_id: null, job_run_id: "job-1", repo_id: "repo-1", limit_claim_id: null, cancel_requested_at: null, control_state: "active", runtime_command_id: "cmd_123", tool_calls_count: 1, tool_calls: [ { name: "postComment", input_preview: "Review comment", output_preview: "comment created", }, ], metadata: { agent_id: "agent-1", sandbox_record_id: "sbx-1" }, events: [ { id: "call-event-1", ai_call_id: "call-1", user_id: "user-1", conversation_id: null, repo_id: "repo-1", event_type: "started", tool_name: null, message: "Agent started", payload: {}, created_at: "2026-03-28T11:01:05.000Z", }, { id: "call-event-2", ai_call_id: "call-1", user_id: "user-1", conversation_id: null, repo_id: "repo-1", event_type: "finished", tool_name: null, message: "Agent finished", payload: { finishReason: "stop" }, created_at: "2026-03-28T11:01:50.000Z", }, ], }, ], review_findings: [], }; await page.route("**/api/auth/user", (route) => fulfillJson(route, { user: connectedUser }) ); await page.route("**/api/settings", (route) => fulfillJson(route, { default_model: "minimax/minimax-m2.5", theme: "dark", }) ); await page.route("**/api/models", (route) => fulfillJson(route, { models: [ { id: "minimax/minimax-m2.5", provider: "minimax", name: "MiniMax M2.5", context_length: 200000, capabilities: ["text"], is_available: true, }, ], catalog: [ { id: "minimax/minimax-m2.5", provider: "minimax", name: "MiniMax M2.5", context_length: 200000, capabilities: ["text"], is_available: true, is_enabled: true, }, ], }) ); await page.route("**/api/agents", (route) => fulfillJson(route, [ { id: "agent-1", name: "NEXTJS-REVIEWER", slug: "nextjs-reviewer", model: "minimax/minimax-m2.5", }, ]) ); await page.route("**/api/github/installations", (route) => fulfillJson(route, [ { id: "inst-1", installation_id: 101, account_login: "webrenew", repositories: [{ id: "repo-1", full_name: "webrenew/blackbox" }], }, ]) ); await page.route("**/api/flows", (route) => fulfillJson(route, [flowPayload]) ); await page.route("**/api/flows/flow-1", (route) => fulfillJson(route, flowPayload) ); await page.route("**/api/flows/flow-1/runs?limit=12", (route) => fulfillJson(route, { runs: [runSummary] }) ); await page.route("**/api/flows/flow-1/runs/job-1", (route) => fulfillJson(route, { run: runDetail }) ); await page.goto("/alex/workflows"); await page.waitForLoadState("networkidle"); await expect(page.getByTestId("flow-assistant-panel")).toBeVisible(); await page.getByTestId("flows-runs-tab").click(); await expect(page.getByTestId("flows-runs-tab")).toHaveAttribute( "aria-selected", "true" ); await expect(page).toHaveURL(/\/alex\/workflows\?tab=runs$/); await expect(page.getByText("Run history")).toBeVisible(); await page.reload(); await page.waitForLoadState("networkidle"); await expect(page).toHaveURL(/\/alex\/workflows\?tab=runs$/); await expect(page.getByTestId("flows-runs-tab")).toHaveAttribute( "aria-selected", "true" ); await expect(page.getByText("Run history")).toBeVisible(); const runCard = page.getByTestId("flow-run-card-job-1"); await expect(runCard).toBeVisible(); await expect(runCard).not.toContainText("run_123"); await expect(runCard).not.toContainText("trigger · run_123"); await expect(runCard.getByRole("button", { name: "Retry" })).toBeVisible(); await expect(runCard.getByRole("button", { name: "Repair" })).toHaveCount(0); await expect(runCard.getByRole("button", { name: "Cancel" })).toHaveCount(0); await runCard.click(); const dialog = page.locator("[data-slot='dialog-content']"); await expect(dialog.getByText("Run details", { exact: true })).toBeVisible(); const dialogBox = await dialog.boundingBox(); expect(dialogBox?.width ?? 0).toBeGreaterThan(1000); await expect(dialog.getByText("Dispatch timeline")).toBeVisible(); await expect( dialog.getByText("Node execution", { exact: true }) ).toBeVisible(); await expect(dialog.getByText("AI calls", { exact: true })).toBeVisible(); await expect(dialog.getByText("Runtime provider")).toHaveCount(0); await expect(dialog.getByText("Runtime handle")).toHaveCount(0); await expect(dialog.getByText("run_123")).toHaveCount(0); await expect( dialog.getByText("PR OPENED", { exact: true }).first() ).toBeVisible(); await expect(dialog.getByRole("link", { name: "PR #42" })).toHaveAttribute( "href", "https://github.com/webrenew/blackbox/pull/42" ); await expect(dialog.getByText("v1", { exact: true })).toBeVisible(); await expect( dialog.getByText(draftTest ? "Draft test snapshot" : "Automation version", { exact: true, }) ).toBeVisible(); await expect( dialog.getByText("Integration · Webrenew agent", { exact: true }) ).toBeVisible(); await expect( dialog.getByText("mogplex/automation-0123456789abcdef", { exact: true }) ).toBeVisible(); await expect(dialog).toContainText('"slug": "acme-plumbing"'); await expect( dialog.getByRole("link", { name: "Open transcript" }) ).toHaveAttribute("href", "/alex/observability?call_id=call-1"); await expect( dialog.getByRole("link", { name: "Open sandbox" }) ).toHaveAttribute( "href", "/alex/observability?repo_id=repo-1&sandbox_record_id=sbx-1" ); await expect(dialog.getByText("Edits", { exact: true })).toBeVisible(); await expect( dialog.getByText("src/widget.ts", { exact: true }).first() ).toBeVisible(); await expect(dialog.getByRole("link", { name: "abcdef1" })).toHaveAttribute( "href", "https://github.com/webrenew/blackbox/commit/abcdef1234567890" ); await expect( dialog .locator("[data-line-type='change-addition']") .filter({ hasText: "export const widget = 'new'" }) ).toBeVisible(); await expect(dialog.getByRole("button", { name: "Retry" })).toBeVisible(); await expect(dialog.getByRole("button", { name: "Repair" })).toHaveCount(0); await expect(dialog.getByRole("button", { name: "Cancel" })).toHaveCount(0); await expect( dialog .getByText("Reported lint failures and suggested the exact fix path.") .first() ).toBeVisible(); await expect(dialog).toContainText("Lint failed on CI"); await dialog.getByText("Tool calls").click(); await expect(dialog.getByText("postComment")).toBeVisible(); await dialog.getByText("Call events").click(); await expect(dialog.getByText("Agent finished")).toBeVisible(); }