import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { connect } from "node:net"; import { join } from "node:path"; import { chromium, type Page } from "playwright"; import { createRouteRegistry, MODULES } from "../../src/components/frontend/src/navigation"; import { CODE_QUEUE_LIVENESS_CHECK_NAMES, runCodeQueueLivenessFixtureChecks } from "./code-queue-liveness-fixtures"; import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot, rootPath } from "./config"; import { boundedJsonDetail } from "./preview"; type CheckStatus = "passed" | "failed"; interface E2ECheck { name: string; status: CheckStatus; detail: unknown; } interface PublicUrls { frontendUrl: string; providerIngressHealthUrl: string; providerIngressWsUrl: string; blockedCoreUrl: string; blockedDatabaseHost: string; blockedDatabasePort: number; } interface E2EApiClient { getJson(path: string, init?: { method?: string; body?: unknown }): unknown; } export interface E2ERunOptions { only: string[]; skip: string[]; } const NETWORK_CHECK_NAMES = [ "network:only-frontend-provider-ports", "network:core-public-blocked", "network:database-public-blocked", "network:findjob-public-blocked", "network:met-nonlinear-public-blocked", "network:claudeqq-public-blocked", "network:todo-note-public-blocked", "network:code-queue-public-blocked", "network:oa-event-flow-public-blocked", "network:decision-center-public-blocked", "network:filebrowser-public-blocked", ] as const; const SERVICE_CHECK_NAMES = [ "core:internal-overview", "core:pgdata-usage", "core:performance-api", "provider:self-node-online", "provider:gateway-version-label", "provider:system-status", "provider:process-resource-status", "provider:docker-status", "provider:gateway-restart-policy", "provider:upgrade-plan", "provider-ingress:public-health", "microservice:catalog-findjob", "microservice:catalog-pipeline", "microservice:catalog-met-nonlinear", "microservice:catalog-claudeqq", "microservice:catalog-todo-note", "microservice:catalog-oa-event-flow", "microservice:catalog-code-queue", "microservice:catalog-decision-center", "microservice:k3sctl-adapter-status", "microservice:k3sctl-control-plane", "microservice:catalog-filebrowser", "microservice:filebrowser-health", "microservice:filebrowser-webui", "microservice:filebrowser-d601-health", "microservice:findjob-status", "microservice:findjob-health", "microservice:findjob-summary", "microservice:findjob-jobs-preview", "microservice:pipeline-status", "microservice:pipeline-health", "microservice:pipeline-snapshot", "microservice:pipeline-oa-event-flow", "microservice:met-nonlinear-status", "microservice:met-nonlinear-health", "microservice:met-nonlinear-queue", "microservice:met-nonlinear-projects", "microservice:met-nonlinear-image", "microservice:claudeqq-status", "microservice:claudeqq-health", "microservice:claudeqq-napcat-login", "microservice:claudeqq-events", "microservice:claudeqq-subscriptions", "microservice:todo-note-status", "microservice:todo-note-health", "microservice:todo-note-migrated-data", "microservice:todo-note-write-path", "microservice:code-queue-status", "microservice:code-queue-health", "microservice:code-queue-workdirs", "microservice:code-queue-tasks", "microservice:decision-center-status", "microservice:decision-center-health", "microservice:decision-center-records", "microservice:decision-center-record-crud", "microservice:decision-center-diary-lifecycle", "microservice:oa-event-flow-status", "microservice:oa-event-flow-health", "microservice:oa-event-flow-diagnostics", "microservice:oa-event-flow-events", "microservice:oa-event-flow-pipeline-bridge", "microservice:oa-event-flow-stats", ] as const; const DATABASE_CHECK_NAMES = [ "database:named-volume-write", "database:provider-state", "database:todo-note-pg-storage", ] as const; const FRONTEND_CHECK_NAMES = [ "frontend:login-provider-visible", "frontend:public-provider-info-visible", "frontend:dev-auth-api-cookie", "frontend:sidebar-collapse", "frontend:mobile-nav-fixed-height", "frontend:mobile-content-top-aligned", "frontend:pending-task-drilldown", "frontend:task-history-diagnostics", "frontend:no-naked-json-before-click", "frontend:raw-json-explicit-button", "frontend:performance-panel-visible", "frontend:system-monitor-visible", "frontend:process-resource-sorting", "frontend:upgrade-plan-dispatch", "frontend:docker-status-visible", "frontend:gateway-version-records-visible", "frontend:gateway-duration-subsecond-visible", "frontend:provider-operation-availability-visible", "frontend:overview-pgdata-visible", "frontend:microservice-catalog-visible", "frontend:todo-note-integrated-visible", "frontend:findjob-integrated-visible", "frontend:oa-event-flow-visible", "frontend:decision-center-visible", "frontend:decision-center-demand-management-visible", "frontend:decision-center-diary-visible", "frontend:code-queue-integrated-visible", "frontend:code-queue-workdirs-loaded", "frontend:code-queue-enqueue-await-smoke", "frontend:code-queue-summary-mobile-wrap", "frontend:code-queue-long-prompt-observation", "frontend:code-queue-initial-prompt-full-expand", "frontend:code-queue-trace-full-load", "frontend:code-queue-judge-wrap", "frontend:code-queue-error-red-markers", "frontend:code-queue-stats-visible", "frontend:code-queue-stats-degraded-visible", "frontend:code-queue-split-brain-live-not-failed", "frontend:code-queue-split-brain-stale-failed", "frontend:code-queue-retry-attempt-trace-current", "frontend:code-queue-step-missing-diagnostic", "frontend:code-queue-judge-feedback-attempt-order", "frontend:claudeqq-integrated-visible", "frontend:url-route-deeplink", "frontend:pipeline-integrated-visible", "frontend:pipeline-react-flow-visible", "frontend:pipeline-sidebars-collapsible", "frontend:pipeline-gantt-defaults", "frontend:pipeline-gantt-frontend-y-accuracy", "frontend:pipeline-gantt-export", "frontend:pipeline-gantt-observation-live-running", "frontend:pipeline-step-timeline-visible", "frontend:pipeline-oa-event-flow-visible", "frontend:pipeline-minimax-quota-visible", "frontend:met-nonlinear-integrated-visible", "frontend:met-nonlinear-project-tree-detail", "frontend:met-nonlinear-queue-detail-speed", "frontend:layout-overflow-desktop", "frontend:layout-overflow-mobile", "frontend:no-console-errors", ] as const; const CODE_QUEUE_FIXTURE_CHECK_NAMES = [...CODE_QUEUE_LIVENESS_CHECK_NAMES] as const; const CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES = [ "frontend:code-queue-retry-attempt-trace-current", "frontend:code-queue-step-missing-diagnostic", "frontend:code-queue-judge-feedback-attempt-order", ] as const; const CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES = [ "frontend:code-queue-stats-visible", "frontend:code-queue-stats-degraded-visible", ] as const; const CODE_QUEUE_LIVENESS_UI_FIXTURE_CHECK_NAMES = [ "frontend:code-queue-split-brain-live-not-failed", "frontend:code-queue-split-brain-stale-failed", ] as const; const ALL_E2E_CHECK_NAMES = [ ...NETWORK_CHECK_NAMES, ...SERVICE_CHECK_NAMES, ...DATABASE_CHECK_NAMES, ...FRONTEND_CHECK_NAMES, ...CODE_QUEUE_FIXTURE_CHECK_NAMES, ] as const; function uniqueText(values: string[]): string[] { const seen = new Set(); const ordered: string[] = []; for (const value of values) { if (!value || seen.has(value)) continue; seen.add(value); ordered.push(value); } return ordered; } function listOptionValues(args: string[], name: string): string[] { const values: string[] = []; for (let index = 0; index < args.length; index += 1) { if (args[index] !== name) continue; const raw = args[index + 1]; if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${name} requires a non-empty value`); values.push(raw); } return uniqueText(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)); } export function parseE2ERunOptions(args: string[]): E2ERunOptions { return { only: listOptionValues(args, "--only"), skip: listOptionValues(args, "--skip"), }; } function pipelineSnapshotConfigObject(pipeline: any): Record { return pipeline && typeof pipeline?.config === "object" && !Array.isArray(pipeline.config) ? pipeline.config as Record : {}; } function pipelineSnapshotNodeIds(value: any): string[] { if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : String(item?.id || item?.nodeId || "")).filter(Boolean); if (Array.isArray(value?.nodes)) return pipelineSnapshotNodeIds(value.nodes); if (Array.isArray(value?.nodeIds)) return pipelineSnapshotNodeIds(value.nodeIds); return []; } function pipelineSnapshotEdges(pipeline: any): Array<{ source: string; target: string; edgeType: string }> { const config = pipelineSnapshotConfigObject(pipeline); const rawEdges = Array.isArray(config.edges) ? config.edges : Array.isArray(pipeline?.edges) ? pipeline.edges : []; const edges: Array<{ source: string; target: string; edgeType: string }> = rawEdges.map((edge: any) => ({ source: String(edge?.from || edge?.source || ""), target: String(edge?.to || edge?.target || ""), edgeType: String(edge?.edgeType || ""), })); return edges.filter((edge) => edge.source.length > 0 && edge.target.length > 0); } function pipelineSnapshotNodes(pipeline: any): string[] { const config = pipelineSnapshotConfigObject(pipeline); const rawNodes = Array.isArray(config.nodes) ? config.nodes : Array.isArray(pipeline?.nodes) ? pipeline.nodes : []; const nodeIds = new Set(rawNodes.map((node: any) => String(node?.id || node?.nodeId || "")).filter((nodeId: string) => nodeId.length > 0)); const rawBatches = Array.isArray(config.topologicalBatches) ? config.topologicalBatches : Array.isArray(pipeline?.topologicalBatches) ? pipeline.topologicalBatches : []; for (const batch of rawBatches) pipelineSnapshotNodeIds(batch).forEach((nodeId) => nodeIds.add(nodeId)); for (const edge of pipelineSnapshotEdges(pipeline)) { nodeIds.add(edge.source); nodeIds.add(edge.target); } return Array.from(nodeIds); } function pipelineSnapshotMonitorNodeIds(pipeline: any): string[] { const config = pipelineSnapshotConfigObject(pipeline); const rawNodes = Array.isArray(config.nodes) ? config.nodes : Array.isArray(pipeline?.nodes) ? pipeline.nodes : []; return rawNodes.map((node: any) => { const nodeId = String(node?.id || node?.nodeId || ""); const kind = String(node?.kind || "").toLowerCase(); const monitor = node?.instanceInputs?.monitor; const componentRef = node?.componentRef || {}; const componentId = String(componentRef?.id || componentRef?.componentClass || ""); const isMonitor = kind === "procedure" && (node?.instanceInputs?.monitorMode === true || monitor?.enabled === true || componentId.toLowerCase().includes("monitor")); return isMonitor ? nodeId : ""; }).filter(Boolean); } function pipelineOrderWithLeadingMonitors(nodeIds: string[], monitorNodeIds: string[]): string[] { const monitorSet = new Set(monitorNodeIds); const leading = monitorNodeIds.filter((nodeId) => nodeIds.includes(nodeId)); return leading.length === 0 ? nodeIds : [...leading, ...nodeIds.filter((nodeId) => !monitorSet.has(nodeId))]; } function pipelineSnapshotNodeOrder(pipeline: any): string[] { const config = pipelineSnapshotConfigObject(pipeline); const rawBatches = Array.isArray(config.topologicalBatches) ? config.topologicalBatches : Array.isArray(pipeline?.topologicalBatches) ? pipeline.topologicalBatches : []; const explicit = rawBatches.map((batch: any) => pipelineSnapshotNodeIds(batch)).filter((batch: string[]) => batch.length > 0); const monitorNodeIds = pipelineSnapshotMonitorNodeIds(pipeline); if (explicit.length > 0) return pipelineOrderWithLeadingMonitors(explicit.flatMap((batch: string[]) => batch), monitorNodeIds); const nodeIds = pipelineSnapshotNodes(pipeline); const idSet = new Set(nodeIds); const incoming = new Map(nodeIds.map((nodeId) => [nodeId, 0])); const outgoing = new Map(nodeIds.map((nodeId) => [nodeId, [] as string[]])); for (const edge of pipelineSnapshotEdges(pipeline).filter((item) => item.edgeType.toLowerCase() !== "rework")) { if (!idSet.has(edge.source) || !idSet.has(edge.target)) continue; outgoing.get(edge.source)?.push(edge.target); incoming.set(edge.target, (incoming.get(edge.target) || 0) + 1); } const levels = new Map(); const queue = nodeIds.filter((nodeId) => (incoming.get(nodeId) || 0) === 0); for (const nodeId of queue) levels.set(nodeId, 0); while (queue.length > 0) { const current = queue.shift()!; const nextLevel = (levels.get(current) || 0) + 1; for (const next of outgoing.get(current) || []) { incoming.set(next, Math.max(0, (incoming.get(next) || 0) - 1)); levels.set(next, Math.max(levels.get(next) || 0, nextLevel)); if ((incoming.get(next) || 0) === 0) queue.push(next); } } nodeIds.forEach((nodeId) => { if (!levels.has(nodeId)) levels.set(nodeId, 0); }); const maxLevel = Math.max(0, ...Array.from(levels.values())); return pipelineOrderWithLeadingMonitors(Array.from({ length: maxLevel + 1 }, (_item, level) => nodeIds.filter((nodeId) => levels.get(nodeId) === level)).flatMap((batch) => batch), monitorNodeIds); } type OverflowProbe = { viewport: string; label: string; path: string; testId: string; ok: boolean; documentOverflowX: number; offenders: Array<{ overflowKind: string; overflowPx: number; tag: string; selector: string; className: string; testId: string; text: string; left: number; right: number; width: number; clientWidth: number; scrollWidth: number; parentClassName: string; }>; }; const LAYOUT_OVERFLOW_PAGE_TEST_IDS: Record = { "/ops/status/": "overview-page", "/ops/performance/": "performance-page", "/nodes/monitor/": "node-monitor-page", "/nodes/docker/": "docker-status-page", "/nodes/gateway/": "gateway-version-page", "/tasks/pending/": "pending-task-page", "/tasks/history/": "task-history-page", "/tasks/scheduled/": "scheduled-task-page", "/app/catalog/": "microservice-catalog-page", "/app/todo-note/": "todo-note-page", "/app/findjob/": "findjob-page", "/app/pipeline/": "pipeline-page", "/app/met-nonlinear/": "met-nonlinear-page", "/app/claudeqq/": "claudeqq-page", "/app/oa-event-flow/": "oa-event-flow-page", "/app/decision-center/": "decision-center-page", "/app/code-queue/": "code-queue-page", }; function layoutOverflowTargets(): Array<{ label: string; path: string; testId: string }> { const registry = createRouteRegistry(MODULES); return registry.modules.flatMap((module) => module.tabs.map((tab) => ({ label: `${module.id}:${tab.id}`, path: tab.canonicalPath, testId: LAYOUT_OVERFLOW_PAGE_TEST_IDS[tab.canonicalPath] || "app-shell", }))); } async function collectLayoutOverflow(page: Page, viewport: { width: number; height: number }, viewportName: string): Promise { const targets = layoutOverflowTargets(); const origin = new URL(page.url()).origin; const results: OverflowProbe[] = []; await page.setViewportSize(viewport); for (const target of targets) { await page.goto(`${origin}${target.path}`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector(`[data-testid="${target.testId}"]`, { timeout: 15000 }).catch(() => undefined); await page.waitForLoadState("networkidle", { timeout: 1200 }).catch(() => undefined); await page.waitForTimeout(180); const result = await page.evaluate(({ viewportName: currentViewport, label, path, testId }) => { const overflowTolerancePx = 6; const viewportWidth = document.documentElement.clientWidth; const checkNestedContainerOverflow = currentViewport === "mobile" || viewportWidth <= 600; const documentOverflowX = Math.max(0, document.documentElement.scrollWidth - viewportWidth, document.body.scrollWidth - viewportWidth); const intentionalHorizontalScrollSelector = [ ".raw-json", ".performance-table-wrap", ".process-table-wrap", ".docker-table-wrap", ".table-wrap", ".met-project-table", ".pipeline-flow-frame", ".react-flow", ".pipeline-gantt-viewport", ".codex-edit-diff", ".tabs", ".rail", ".status-strip", ".top-status-bar", ".codex-transcript-item.ran .codex-transcript-command", ".codex-transcript-item.ran .codex-transcript-body", ".codex-transcript-item.explored .codex-transcript-command", ".codex-transcript-item.explored .codex-transcript-body", ".codex-transcript-item.edited .codex-transcript-command", ".codex-transcript-item.edited .codex-transcript-body", ].join(","); const insideIntentionalHorizontalScroll = (element: Element): boolean => Boolean(element.closest(intentionalHorizontalScrollSelector)); const selectorFor = (html: HTMLElement): string => { const tag = html.tagName.toLowerCase(); const test = html.getAttribute("data-testid"); const id = html.id ? `#${html.id}` : ""; const className = String(html.className || ""); const classes = className .split(/\s+/) .map((item) => item.trim()) .filter(Boolean) .slice(0, 3) .map((item) => `.${item}`) .join(""); return `${tag}${id}${classes}${test ? `[data-testid="${test}"]` : ""}`; }; const offenderFor = (html: HTMLElement, overflowKind: string, overflowPx: number) => { const rect = html.getBoundingClientRect(); const parent = html.parentElement; return { overflowKind, overflowPx: Math.round(overflowPx), tag: html.tagName.toLowerCase(), selector: selectorFor(html).slice(0, 180), className: String(html.className || "").slice(0, 160), testId: html.getAttribute("data-testid") || "", text: (html.innerText || html.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160), left: Math.round(rect.left), right: Math.round(rect.right), width: Math.round(rect.width), clientWidth: Math.round(html.clientWidth), scrollWidth: Math.round(html.scrollWidth), parentClassName: String(parent?.className || "").slice(0, 160), }; }; const offenders = Array.from(document.body.querySelectorAll("*")).flatMap((element) => { const html = element as HTMLElement; if (!(html instanceof HTMLElement) || insideIntentionalHorizontalScroll(html)) return []; const style = getComputedStyle(html); const rect = html.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return []; const excessLeft = rect.left < -overflowTolerancePx; const excessRight = rect.right > viewportWidth + overflowTolerancePx; const viewportOverflowPx = Math.max(0, -rect.left, rect.right - viewportWidth); const containerOverflowPx = Math.max(0, html.scrollWidth - html.clientWidth); const formControl = ["INPUT", "SELECT", "TEXTAREA"].includes(html.tagName); const clippedOverflow = ["clip", "hidden"].includes(style.overflowX); const found: ReturnType[] = []; if (excessLeft || excessRight) found.push(offenderFor(html, "viewport", viewportOverflowPx)); if (checkNestedContainerOverflow && !formControl && !clippedOverflow && html.clientWidth > 0 && containerOverflowPx > overflowTolerancePx) found.push(offenderFor(html, "container", containerOverflowPx)); return found; }) .sort((a, b) => b.overflowPx - a.overflowPx) .slice(0, 20); return { viewport: currentViewport, label, path, testId, ok: documentOverflowX <= overflowTolerancePx && offenders.length === 0, documentOverflowX: Math.round(documentOverflowX), offenders, }; }, { viewportName, label: target.label, path: target.path, testId: target.testId }); results.push(result); } return results; } function escapedPatternRegex(value: string): string { return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); } function matchCheckPattern(name: string, pattern: string): boolean { const normalizedName = String(name || ""); const normalizedPattern = String(pattern || "").trim(); if (!normalizedPattern) return false; if (normalizedPattern.includes("*")) { const regex = new RegExp(`^${escapedPatternRegex(normalizedPattern).replace(/\\\*/g, ".*")}$`); return regex.test(normalizedName); } return normalizedName === normalizedPattern || normalizedName.startsWith(`${normalizedPattern}:`) || normalizedName.startsWith(`${normalizedPattern}-`); } function wantsCheck(options: E2ERunOptions, name: string): boolean { const included = options.only.length === 0 || options.only.some((pattern) => matchCheckPattern(name, pattern)); const skipped = options.skip.some((pattern) => matchCheckPattern(name, pattern)); return included && !skipped; } function wantsAnyCheck(options: E2ERunOptions, names: string[]): boolean { return names.some((name) => wantsCheck(options, name)); } function wantsPrefix(options: E2ERunOptions, prefix: string): boolean { if (options.only.length === 0) return !options.skip.some((pattern) => matchCheckPattern(`${prefix}:probe`, pattern) || matchCheckPattern(prefix, pattern)); return options.only.some((pattern) => { const normalizedPattern = String(pattern || "").trim(); return normalizedPattern === prefix || normalizedPattern.startsWith(`${prefix}:`) || normalizedPattern.startsWith(`${prefix}-`) || matchCheckPattern(`${prefix}:probe`, normalizedPattern); }) && !options.skip.some((pattern) => matchCheckPattern(`${prefix}:probe`, pattern) || matchCheckPattern(prefix, pattern)); } function publicUrls(config: UniDeskConfig): PublicUrls { const frontendTarget = String(process.env.UNIDESK_E2E_FRONTEND || "prod").trim().toLowerCase(); const frontendPort = frontendTarget === "dev" ? config.network.devFrontend.port : config.network.frontend.port; const frontendUrlOverride = String(process.env.UNIDESK_E2E_FRONTEND_URL || "").trim(); return { frontendUrl: frontendUrlOverride || `http://${config.network.publicHost}:${frontendPort}`, providerIngressHealthUrl: `http://${config.network.publicHost}:${config.network.providerIngress.port}/health`, providerIngressWsUrl: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`, blockedCoreUrl: `http://${config.network.publicHost}:${config.network.core.port}`, blockedDatabaseHost: config.network.publicHost, blockedDatabasePort: config.network.database.port, }; } function isDevFrontendTarget(): boolean { return String(process.env.UNIDESK_E2E_FRONTEND || "prod").trim().toLowerCase() === "dev"; } async function fetchProbe(url: string, timeoutMs = 8000): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); const text = await response.text(); let body: unknown = text; try { body = text.length > 0 ? JSON.parse(text) : null; } catch { /* keep text body */ } return { reachable: true, ok: response.ok, status: response.status, body }; } catch (error) { return { reachable: false, ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { clearTimeout(timer); } } function tcpProbe(host: string, port: number, timeoutMs = 2500): Promise { return new Promise((resolve) => { const socket = connect({ host, port }); let settled = false; const finish = (detail: unknown): void => { if (settled) return; settled = true; socket.destroy(); resolve(detail); }; socket.setTimeout(timeoutMs); socket.once("connect", () => finish({ reachable: true, ok: true, host, port })); socket.once("timeout", () => finish({ reachable: false, ok: false, host, port, error: "timeout" })); socket.once("error", (error) => finish({ reachable: false, ok: false, host, port, error: error.message })); }); } function rawHttpProbe(urlText: string, timeoutMs = 5000): Promise { return new Promise((resolve) => { let parsed: URL; try { parsed = new URL(urlText); } catch (error) { resolve({ reachable: false, ok: false, error: error instanceof Error ? error.message : String(error) }); return; } if (parsed.protocol !== "http:") { resolve({ reachable: false, ok: false, error: `unsupported protocol ${parsed.protocol}` }); return; } const host = parsed.hostname; const port = Number(parsed.port || 80); const path = `${parsed.pathname || "/"}${parsed.search || ""}`; const socket = connect({ host, port }); const chunks: Buffer[] = []; let settled = false; const finish = (detail: unknown): void => { if (settled) return; settled = true; socket.destroy(); resolve(detail); }; socket.setTimeout(timeoutMs); socket.once("connect", () => { socket.write([ `GET ${path} HTTP/1.1`, `Host: ${parsed.host}`, "Accept: application/json", "Connection: close", "", "", ].join("\r\n")); }); socket.on("data", (chunk) => chunks.push(Buffer.from(chunk))); socket.once("timeout", () => finish({ reachable: false, ok: false, error: "timeout", host, port })); socket.once("error", (error) => finish({ reachable: false, ok: false, error: error.message, host, port })); socket.once("end", () => { const raw = Buffer.concat(chunks).toString("utf8"); const separatorIndex = raw.indexOf("\r\n\r\n"); const headerText = separatorIndex >= 0 ? raw.slice(0, separatorIndex) : raw; const bodyText = separatorIndex >= 0 ? raw.slice(separatorIndex + 4) : ""; const status = Number(headerText.match(/^HTTP\/\d(?:\.\d)?\s+(\d+)/u)?.[1] || 0); let body: unknown = bodyText; try { body = bodyText.length > 0 ? JSON.parse(bodyText) : null; } catch { /* keep text body */ } finish({ reachable: true, ok: status >= 200 && status < 300, status, body }); }); socket.once("close", () => { if (settled) return; const raw = Buffer.concat(chunks).toString("utf8"); finish({ reachable: raw.length > 0, ok: false, status: 0, body: raw, error: raw.length > 0 ? "closed before complete response" : "closed" }); }); }); } function addCheck(checks: E2ECheck[], name: string, passed: boolean, detail: unknown): void { checks.push({ name, status: passed ? "passed" : "failed", detail: boundedJsonDetail(detail, passed ? 4_000 : 24_000, { maxDepth: passed ? 3 : 5, maxArrayItems: passed ? 3 : 8, maxObjectKeys: passed ? 12 : 40, maxStringLength: passed ? 600 : 1600, }), }); } function addSelectedCheck(checks: E2ECheck[], options: E2ERunOptions, name: string, passed: boolean, detail: unknown): void { if (!wantsCheck(options, name)) return; addCheck(checks, name, passed, detail); } function codeQueueFixtureChecks(checks: E2ECheck[], options: E2ERunOptions): void { const selected = CODE_QUEUE_FIXTURE_CHECK_NAMES.filter((name) => wantsCheck(options, name)); const result = runCodeQueueLivenessFixtureChecks(selected); for (const check of result.checks) { addSelectedCheck(checks, options, check.name, check.ok, check.detail); } } function safeTestId(value: string): string { return value.replace(/[^a-zA-Z0-9_-]/g, "_"); } function providerGatewayPackageVersion(): string { try { const raw = readFileSync(rootPath("src", "components", "provider-gateway", "package.json"), "utf8"); const parsed = JSON.parse(raw) as { version?: unknown }; return typeof parsed.version === "string" ? parsed.version : ""; } catch { return ""; } } function isCodeQueueTaskEnqueueRequest(url: string, method: string): boolean { try { const parsed = new URL(url); return method === "POST" && parsed.pathname === "/api/microservices/code-queue/proxy/api/tasks"; } catch { return false; } } async function runCodeQueueEnqueueAwaitSmoke(page: Page): Promise { const marker = `e2e-await-enqueue-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; const submitQueueId = "e2e-submit-smoke"; const prompt = [ `Code Queue await enqueue smoke ${marker}`, "", "This task is created by the frontend E2E smoke test to verify that the enqueue submit path awaits the backend response before unlocking the form.", ].join("\n"); let delayedPostCount = 0; const taskOverviewRequests: string[] = []; const routePattern = "**/api/microservices/code-queue/proxy/api/tasks**"; const routeHandler = async (route: any, request: any): Promise => { if (!isCodeQueueTaskEnqueueRequest(request.url(), request.method())) { await route.continue(); return; } delayedPostCount += 1; await new Promise((resolve) => setTimeout(resolve, 900)); await route.continue(); }; const onRequest = (request: any): void => { try { const parsed = new URL(request.url()); if (request.method() === "GET" && parsed.pathname === "/api/microservices/code-queue/proxy/api/tasks/overview") { taskOverviewRequests.push(parsed.search); } } catch { // ignore non-URL requests } }; page.on("request", onRequest); await page.route(routePattern, routeHandler); try { await page.getByTestId("code-queue-filter-select").selectOption("__all__").catch(() => undefined); page.once("dialog", (dialog) => { void dialog.accept(submitQueueId); }); await page.getByTestId("codex-create-queue-button").click(); await page.waitForFunction((queueId) => { const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const filter = document.querySelector('[data-testid="code-queue-filter-select"]') as HTMLSelectElement | null; return select?.value === queueId && filter?.value === queueId && Array.from(select?.options || []).some((option) => option.value === queueId); }, submitQueueId, { timeout: 10000 }); await page.getByTestId("code-queue-filter-select").selectOption("__all__").catch(() => undefined); await page.getByTestId("code-queue-id-select").selectOption(submitQueueId); taskOverviewRequests.length = 0; const beforeSubmitView = await page.evaluate(() => { const filter = document.querySelector('[data-testid="code-queue-filter-select"]') as HTMLSelectElement | null; const submitQueue = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; return { filterValue: filter?.value || "", submitQueueValue: submitQueue?.value || "", }; }); await page.getByTestId("codex-max-attempts-input").fill("1"); await page.getByTestId("codex-repeat-count-input").fill("1"); await page.locator('[data-testid="code-queue-task-form"] textarea').fill(prompt); await page.waitForFunction(() => { const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return button !== null && !button.disabled && (button.textContent || "").includes("入队"); }, undefined, { timeout: 5000 }); const requestPromise = page.waitForRequest((request) => isCodeQueueTaskEnqueueRequest(request.url(), request.method()), { timeout: 10000 }); const responsePromise = page.waitForResponse((response) => isCodeQueueTaskEnqueueRequest(response.url(), response.request().method()), { timeout: 30000 }); await page.getByTestId("codex-enqueue-button").click(); const request = await requestPromise; await page.waitForSelector('[data-testid="codex-submit-wait"]', { timeout: 5000 }); const duringAwait = await page.evaluate(() => { const form = document.querySelector('[data-testid="code-queue-task-form"]') as HTMLElement | null; const wait = document.querySelector('[data-testid="codex-submit-wait"]') as HTMLElement | null; const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; const textarea = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; return { formBusy: form?.getAttribute("aria-busy") === "true", waitVisible: Boolean(wait && wait.offsetParent !== null && (wait.textContent || "").includes("正在提交")), buttonDisabled: Boolean(button?.disabled), buttonText: button?.textContent || "", textareaDisabled: Boolean(textarea?.disabled), }; }); const response = await responsePromise; const responseBody = await response.json().catch((error: unknown) => ({ parseError: error instanceof Error ? error.message : String(error) })); const taskId = String(responseBody?.tasks?.[0]?.id || ""); await page.waitForFunction((id) => { const form = document.querySelector('[data-testid="code-queue-task-form"]') as HTMLElement | null; const notice = document.querySelector('[data-testid="codex-create-success"]') as HTMLElement | null; return form?.getAttribute("aria-busy") === "false" && (notice?.textContent || "").includes(String(id)); }, taskId, { timeout: 30000 }); const afterAwait = await page.evaluate((id) => { const form = document.querySelector('[data-testid="code-queue-task-form"]') as HTMLElement | null; const wait = document.querySelector('[data-testid="codex-submit-wait"]') as HTMLElement | null; const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; const textarea = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; const notice = document.querySelector('[data-testid="codex-create-success"]') as HTMLElement | null; const card = document.querySelector(`[data-testid="codex-task-${CSS.escape(String(id))}"]`) as HTMLElement | null; const filter = document.querySelector('[data-testid="code-queue-filter-select"]') as HTMLSelectElement | null; const submitQueue = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const sessionPanel = document.querySelector('.codex-output-panel') as HTMLElement | null; const cards = Array.from(document.querySelectorAll('[data-testid^="codex-task-codex_"]')).map((node) => (node as HTMLElement).innerText); return { formBusy: form?.getAttribute("aria-busy") === "true", waitMissing: wait === null, buttonDisabled: Boolean(button?.disabled), textareaDisabled: Boolean(textarea?.disabled), textareaEmpty: (textarea?.value || "") === "", noticeText: notice?.textContent || "", cardVisible: Boolean(card && card.offsetParent !== null), filterValue: filter?.value || "", submitQueueValue: submitQueue?.value || "", selectedTraceHasTask: Boolean(sessionPanel && (sessionPanel.textContent || "").includes(String(id))), allVisibleCardsRespectFilter: filter?.value === "__all__" || cards.every((text) => text.includes(`queue=${filter?.value || ""}`)), }; }, taskId); const storedTask = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" }); const text = await response.text(); let body: any = null; try { body = text ? JSON.parse(text) : null; } catch { body = { text }; } return { ok: response.ok, status: response.status, body }; }, taskId); const interrupt = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}/interrupt`, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: "{}", }); const text = await response.text(); let body: any = null; try { body = text ? JSON.parse(text) : null; } catch { body = { text }; } return { ok: response.ok, status: response.status, body }; }, taskId); const requestBody = (() => { try { return request.postDataJSON(); } catch { return null; } })(); await page.getByTestId("codex-max-attempts-input").fill("99").catch(() => undefined); const afterSubmitOverviewRequests = taskOverviewRequests.slice(); return { checked: true, marker, submitQueueId, delayedPostCount, requestBody, afterSubmitOverviewRequests, postSubmitSubmitQueueOverviewRequestCount: afterSubmitOverviewRequests.filter((search) => search.includes(`queueId=${encodeURIComponent(submitQueueId)}`)).length, responseStatus: response.status(), responseOk: response.ok(), responseBody: { ok: responseBody?.ok === true, taskIds: Array.isArray(responseBody?.tasks) ? responseBody.tasks.map((task: any) => String(task?.id || "")).filter(Boolean) : [], }, taskId, beforeSubmitView, duringAwait, afterAwait, storedTask: { ok: storedTask.ok, status: storedTask.status, id: String(storedTask.body?.task?.id || ""), queueId: String(storedTask.body?.task?.queueId || ""), promptIncludesMarker: String(storedTask.body?.task?.prompt || "").includes(marker), displayPromptIncludesMarker: String(storedTask.body?.task?.displayPrompt || "").includes(marker), taskStatus: String(storedTask.body?.task?.status || ""), }, interrupt: { ok: interrupt.ok, status: interrupt.status, taskStatus: String(interrupt.body?.task?.status || ""), error: typeof interrupt.body?.error === "string" ? interrupt.body.error : "", }, }; } finally { await page.unroute(routePattern, routeHandler).catch(() => undefined); page.off("request", onRequest); } } async function runCodeQueueLongPromptObservation(page: Page): Promise { const marker = `E2E_LONG_PROMPT_TAIL_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; const submitQueueId = "e2e-long-prompt"; const filler = Array.from({ length: 76 }, (_, index) => `long-prompt-line-${String(index + 1).padStart(2, "0")} ${"abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)}`).join("\n"); const prompt = [ "Code Queue long prompt observation regression", "", filler, "", "验收标准:", `必须在所有原始 prompt 观察层保留这个唯一 tail marker: ${marker}`, ].join("\n"); const promptChars = prompt.length; if (promptChars <= 2500) throw new Error(`long prompt fixture too short: ${promptChars}`); const apiFetch = async (path: string, init: RequestInit = {}): Promise => { const response = await fetch(path, { credentials: "same-origin", headers: { "content-type": "application/json", ...(init.headers || {}) }, ...init, }); const text = await response.text(); let body: any = null; try { body = text.length > 0 ? JSON.parse(text) : null; } catch { body = { text }; } return { ok: response.ok, status: response.status, body }; }; await page.getByTestId("code-queue-filter-select").selectOption("__all__").catch(() => undefined); await page.getByTestId("code-queue-id-select").selectOption(submitQueueId).catch(async () => { page.once("dialog", (dialog) => { void dialog.accept(submitQueueId); }); await page.getByTestId("codex-create-queue-button").click(); await page.waitForFunction((queueId) => { const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; return Array.from(select?.options || []).some((option) => option.value === queueId); }, submitQueueId, { timeout: 10000 }); await page.getByTestId("code-queue-id-select").selectOption(submitQueueId); }); await page.getByTestId("codex-max-attempts-input").fill("1"); await page.getByTestId("codex-repeat-count-input").fill("1"); await page.locator('[data-testid="code-queue-task-form"] textarea').fill(prompt); await page.waitForFunction(() => { const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return button !== null && !button.disabled; }, undefined, { timeout: 5000 }); const responsePromise = page.waitForResponse((response) => isCodeQueueTaskEnqueueRequest(response.url(), response.request().method()), { timeout: 30000 }); await page.getByTestId("codex-enqueue-button").click(); const response = await responsePromise; const createBody = await response.json().catch((error: unknown) => ({ parseError: error instanceof Error ? error.message : String(error) })); const createdTask = Array.isArray(createBody?.tasks) ? createBody.tasks[0] : null; const taskId = String(createdTask?.id || ""); if (!taskId) return { checked: true, marker, promptChars, createStatus: response.status(), taskId: "", error: "missing task id", createBody }; await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 30000 }).catch(() => undefined); const overview = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=1&includeActive=1&stats=0&afterSeq=0&preferId=${encodeURIComponent(String(id))}`, { credentials: "same-origin" }); const body = await response.json().catch(() => null); return { ok: response.ok, status: response.status, body }; }, taskId); const meta = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}?meta=1`, { credentials: "same-origin" }); const body = await response.json().catch(() => null); return { ok: response.ok, status: response.status, body }; }, taskId); const initialPrompt = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(id))}/prompt?part=initial`, { credentials: "same-origin" }); const body = await response.json().catch(() => null); return { ok: response.ok, status: response.status, body }; }, taskId); await page.getByTestId(`codex-task-${taskId}`).click().catch(() => undefined); await page.waitForSelector('[data-testid="codex-initial-prompt-base"]', { timeout: 15000 }).catch(() => undefined); const ui = await page.evaluate(() => { const base = document.querySelector('[data-testid="codex-initial-prompt-base"]') as HTMLElement | null; return { baseText: base?.innerText || "", baseChars: (base?.innerText || "").length, }; }); const overviewTasks = Array.isArray(overview.body?.tasks) ? overview.body.tasks : []; const overviewTask = overviewTasks.find((task: any) => String(task?.id || "") === taskId) || null; const selectedTask = overview.body?.selected?.task || null; const metaTask = meta.body?.task || null; const createPrompt = String(createdTask?.prompt || ""); const createDisplayPrompt = String(createdTask?.displayPrompt || ""); const createPreview = createdTask?.displayPromptPreview || createdTask?.promptPreview || null; const overviewPrompt = String(overviewTask?.prompt || ""); const overviewDisplayPrompt = String(overviewTask?.displayPrompt || ""); const overviewPreview = overviewTask?.displayPromptPreview || overviewTask?.promptPreview || null; const selectedDisplayPrompt = String(selectedTask?.displayPrompt || ""); const selectedPreview = selectedTask?.displayPromptPreview || selectedTask?.promptPreview || null; const metaPrompt = String(metaTask?.prompt || ""); const metaBasePrompt = String(metaTask?.basePrompt || ""); const metaDisplayPrompt = String(metaTask?.displayPrompt || ""); const promptText = String(initialPrompt.body?.text || ""); const previewHasTruncationMarker = [createPreview?.text, overviewPreview?.text, selectedPreview?.text].some((value) => String(value || "").includes("...")); const originalValues = [createPrompt, createDisplayPrompt, overviewPrompt, overviewDisplayPrompt, selectedDisplayPrompt, metaPrompt, metaBasePrompt, metaDisplayPrompt, promptText]; await apiFetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST", body: "{}" }).catch(() => null); return { checked: true, marker, taskId, promptChars, createStatus: response.status(), createOk: createBody?.ok === true, createPromptChars: createPrompt.length, createDisplayPromptChars: createDisplayPrompt.length, overviewStatus: overview.status, overviewTaskFound: overviewTask !== null, overviewPromptChars: overviewPrompt.length, overviewDisplayPromptChars: overviewDisplayPrompt.length, selectedTaskFound: selectedTask !== null && String(selectedTask?.id || "") === taskId, selectedDisplayPromptChars: selectedDisplayPrompt.length, metaStatus: meta.status, metaPromptChars: metaPrompt.length, metaBasePromptChars: metaBasePrompt.length, metaDisplayPromptChars: metaDisplayPrompt.length, initialPromptStatus: initialPrompt.status, initialPromptChars: Number(initialPrompt.body?.chars || promptText.length), uiBaseChars: ui.baseChars, preview: { create: createPreview, overview: overviewPreview, selected: selectedPreview, previewHasTruncationMarker, createPromptPreviewFieldPresent: createdTask?.promptPreview !== undefined, overviewPromptPreviewFieldPresent: overviewTask?.promptPreview !== undefined, selectedPromptPreviewFieldPresent: selectedTask?.promptPreview !== undefined, }, checks: { createOriginalHasTail: createPrompt.includes(marker) && createDisplayPrompt.includes(marker), overviewOriginalHasTail: overviewPrompt.includes(marker) && overviewDisplayPrompt.includes(marker), selectedOriginalHasTail: selectedDisplayPrompt.includes(marker), metaOriginalHasTail: metaPrompt.includes(marker) && metaBasePrompt.includes(marker) && metaDisplayPrompt.includes(marker), initialPromptHasTail: promptText.includes(marker), uiFullPromptHasTail: String(ui.baseText || "").includes(marker), originalsHaveNoTruncationMarker: originalValues.every((value) => !value.includes("...")), previewFieldsExplicit: createdTask?.promptPreview !== undefined && overviewTask?.promptPreview !== undefined && selectedTask?.promptPreview !== undefined, previewCanTruncate: Boolean(createPreview?.truncated || overviewPreview?.truncated || selectedPreview?.truncated), }, }; } async function runCodeQueueRetryTraceFixture(page: Page, frontendUrl: string): Promise { const taskId = "codex_2000000000000_issue12_retry_fixture"; const now = new Date().toISOString(); const attempt1Started = "2026-05-20T03:00:00.000Z"; const attempt1Finished = "2026-05-20T03:04:00.000Z"; const attempt2Started = "2026-05-20T03:05:00.000Z"; const traceRequests: string[] = []; const promptRequests: string[] = []; const task = { id: taskId, queueId: "issue12-fixture", queueEnteredAt: attempt2Started, displayPrompt: "Issue #12 retry current attempt fixture", basePrompt: "Issue #12 retry current attempt fixture", displayPromptPreview: "Issue #12 retry current attempt fixture", providerId: "D601-dev", executionMode: "default", cwd: "/workspace-dev", model: "gpt-5.5", maxAttempts: 99, status: "running", createdAt: "2026-05-20T02:59:00.000Z", updatedAt: now, startedAt: attempt1Started, finishedAt: null, currentAttempt: 2, currentMode: "retry", codexThreadId: "thread_issue12_fixture", activeTurnId: "turn_issue12_attempt_2", finalResponse: "", lastJudge: { decision: "retry", confidence: 0.82, reason: "Attempt 1 judge: missing live verification.", continuePrompt: "Continue with attempt 2 and gather live trace evidence.", source: "fixture", }, stepCount: 0, llmStepCount: 0, outputCount: 0, eventCount: 0, attemptCount: 1, attempts: [{ index: 1, mode: "initial", startedAt: attempt1Started, finishedAt: attempt1Finished, terminalStatus: "completed", finalResponsePreview: "Attempt 1 stopped after planning.", finalResponseChars: 33, judge: { decision: "retry", confidence: 0.82, reason: "Attempt 1 judge: missing live verification.", continuePrompt: "Continue with attempt 2 and gather live trace evidence.", source: "fixture", }, feedbackPromptPreview: "Continue with attempt 2 and gather live trace evidence.", feedbackPromptChars: 56, feedbackPromptLines: 1, feedbackPromptSource: "judge-continue-prompt", feedbackPromptForAttempt: 2, statsSource: "unavailable", traceStats: null, execution: { durationMs: 240000, traceLineCount: 5, stepCount: 5, editedFiles: [], commands: ["bun test"], statsSource: "unavailable", traceStats: null, }, }], summaryOnly: true, promptEditable: false, timing: { durationMs: 330000, totalElapsedMs: 390000 }, }; const summary = { id: taskId, taskId, queueId: "issue12-fixture", status: "running", providerId: "D601-dev", executionMode: "default", model: "gpt-5.5", cwd: "/workspace-dev", createdAt: task.createdAt, startedAt: task.startedAt, finishedAt: null, updatedAt: now, currentAttempt: 2, maxAttempts: 99, stepCount: null, llmStepCount: null, retainedStepCount: 0, traceStats: null, statsSource: "unavailable", prompt: { basePrompt: task.basePrompt, basePromptLines: 1, promptChars: task.basePrompt.length, promptLines: 1, }, execution: { durationMs: 390000, traceLineCount: 0, stepCount: null, editedFiles: [], commands: [], statsSource: "unavailable", traceStats: null, }, finalResponse: "", lastJudge: task.lastJudge, attempts: task.attempts, timing: task.timing, }; const overview = { ok: true, queue: { activeTaskId: taskId, activeTaskIds: [taskId], total: 1, unreadTerminal: 0, counts: { running: 1, queued: 0, retry_wait: 0, judging: 0 }, queues: [{ id: "issue12-fixture", name: "Issue 12 Fixture", total: 1, activeTaskId: taskId, counts: { running: 1 } }], mainProviderId: "D601-dev", defaultProviderId: "D601-dev", defaultWorkdir: "/workspace-dev", defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" }, codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"], executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }], executionModes: [{ id: "default", label: "默认容器/本机" }], }, statistics: { timezone: "Asia/Shanghai", range: { startDate: "2026-05-20", endDate: "2026-05-20" }, totals: {}, daily: [] }, tasks: [task], pagination: { returned: 1, total: 1, hasMore: false }, selected: { task, transcript: [], hasMore: false, preview: true }, }; const attempt2Steps = [ { seq: 201, at: "2026-05-20T03:05:20.000Z", kind: "message", title: "Assistant message", status: "streaming", rawSeqs: [201], summaryLines: ["Attempt 2 running: fixture trace is visible."], hasDetail: true, }, { seq: 202, at: "2026-05-20T03:05:40.000Z", kind: "ran", title: "Ran bun --check target files", status: "running", rawSeqs: [202], summaryLines: ["Attempt 2 running command trace should appear before old judge."], hasDetail: true, }, ]; const routePattern = "**/api/microservices/code-queue/proxy/api/**"; const handler = async (route: any, request: any): Promise => { if (request.method() !== "GET") { await route.continue(); return; } const url = new URL(request.url()); const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, ""); if (path === "/api/tasks/overview") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) }); return; } if (path === "/api/workdirs") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) }); return; } if (path === "/api/queues") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) }); return; } if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary }) }); return; } if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-steps` || path === `/api/tasks/${taskId}/trace-steps`) { traceRequests.push(url.search); const attempt = url.searchParams.get("attempt") || ""; const steps = attempt === "2" ? attempt2Steps : []; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, taskId, attempt: attempt ? Number(attempt) : null, source: "fixture", steps, total: steps.length, hasMore: false, nextAfterSeq: steps.at(-1)?.seq || 0 }) }); return; } if (path === `/api/tasks/${encodeURIComponent(taskId)}/prompt` || path === `/api/tasks/${taskId}/prompt`) { promptRequests.push(url.search); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, part: url.searchParams.get("part"), attempt: Number(url.searchParams.get("attempt") || 0), text: "Continue with attempt 2 and gather live trace evidence.", chars: 56, lines: 1, forAttempt: 2 }) }); return; } await route.continue(); }; await page.route(routePattern, handler); try { await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000, state: "attached" }); await page.waitForSelector(`[data-testid="codex-task-${taskId}"]`, { timeout: 15000 }); await page.locator(`[data-testid="codex-task-${taskId}"]`).click(); await page.waitForTimeout(1000); await page.waitForFunction(() => { const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLDetailsElement | null; return current?.getAttribute("data-attempt-index") === "2" && current?.getAttribute("data-running") === "true" && String(current?.textContent || "").includes("执行过程摘要 #2") && String(current?.textContent || "").includes("STEP 2 raw"); }, undefined, { timeout: 15000 }); const beforeExpand = await page.evaluate((id) => { const card = document.querySelector(`[data-testid="codex-task-${id}"]`) as HTMLElement | null; const current = document.querySelector('[data-testid="codex-execution-summary-current"]') as HTMLElement | null; const attempt1Judge = document.querySelector('[data-testid="codex-progressive-judge-attempt-1"]') as HTMLElement | null; const feedback = document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLElement | null; const currentStep = document.querySelector('[data-testid="codex-execution-summary-current-step-count"]') as HTMLElement | null; const root = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const order = ["codex-execution-summary-current", "codex-progressive-judge-attempt-1", "codex-judge-feedback-prompt-attempt-1"] .map((testId) => { const element = document.querySelector(`[data-testid="${testId}"]`); return element && root ? Array.prototype.indexOf.call(root.querySelectorAll("[data-testid]"), element) : -1; }); return { cardText: card?.innerText || "", cardAttemptVisible: (card?.innerText || "").includes("2/99"), cardStepText: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.textContent || "", cardStepState: card?.querySelector('[data-testid^="codex-task-step-count-"]')?.getAttribute("data-step-state") || "", currentText: current?.textContent || "", currentAttempt: current?.getAttribute("data-attempt-index") || "", currentRunning: current?.getAttribute("data-running") || "", currentStepText: currentStep?.textContent || "", currentStepState: currentStep?.getAttribute("data-step-state") || "", attempt1JudgeText: attempt1Judge?.textContent || "", feedbackText: feedback?.textContent || "", currentBeforeJudge: order[0] >= 0 && order[1] >= 0 && order[0] < order[1], judgeBeforeFeedback: order[1] >= 0 && order[2] >= 0 && order[1] < order[2], currentJudgeMissing: document.querySelector('[data-testid="codex-progressive-judge-current"]') === null, legacyCurrentAliasMissing: document.querySelector('[data-testid="codex-progressive-judge"]') === null || (document.querySelector('[data-testid="codex-progressive-judge"]') as HTMLElement | null)?.getAttribute("data-attempt-index") !== "2", }; }, taskId); await page.locator('[data-testid="codex-judge-feedback-prompt-attempt-1"] summary').click(); await page.waitForFunction(() => (document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1"]') as HTMLDetailsElement | null)?.open === true, undefined, { timeout: 5000 }); await page.waitForFunction(() => { const text = document.querySelector('[data-testid="codex-judge-feedback-prompt-attempt-1-text"]')?.textContent || ""; return text.includes("Continue with attempt 2"); }, undefined, { timeout: 5000 }); const feedbackText = await page.getByTestId("codex-judge-feedback-prompt-attempt-1-text").innerText({ timeout: 5000 }); const currentStepRows = await page.locator('[data-testid="codex-execution-summary-current"] .codex-trace-step').count(); return { checked: true, taskId, fixture: { cardAttempt: "2/99", summaryAttempts: summary.attempts.length, attempt2Steps: attempt2Steps.length, }, beforeExpand, currentStepRows, traceRequests, promptRequests, feedbackText, ok: beforeExpand.cardAttemptVisible === true && beforeExpand.currentAttempt === "2" && beforeExpand.currentRunning === "true" && beforeExpand.currentText.includes("执行过程摘要 #2") && beforeExpand.currentText.includes("STEP 2 raw") && beforeExpand.currentBeforeJudge === true && beforeExpand.judgeBeforeFeedback === true && beforeExpand.currentJudgeMissing === true && beforeExpand.cardStepText !== "STEP --" && beforeExpand.currentStepText !== "STEP --" && ["syncing", "fallback", "raw-trace"].includes(beforeExpand.cardStepState) && ["syncing", "fallback", "raw-trace"].includes(beforeExpand.currentStepState) && currentStepRows >= 2 && traceRequests.some((search) => search.includes("attempt=2")) && promptRequests.some((search) => search.includes("part=feedback") && search.includes("attempt=1")) && feedbackText.includes("Continue with attempt 2"), }; } finally { await page.unroute(routePattern, handler).catch(() => undefined); } } function codeQueueStatsFixtureOverview(statistics: any, taskId: string): any { const now = "2026-05-20T05:20:00.000Z"; const task = { id: taskId, queueId: "issue13-stats-fixture", status: "succeeded", providerId: "D601-dev", executionMode: "default", model: "gpt-5.5", cwd: "/workspace-dev", createdAt: "2026-05-20T04:00:00.000Z", startedAt: "2026-05-20T04:01:00.000Z", finishedAt: "2026-05-20T04:08:00.000Z", updatedAt: now, displayPromptPreview: "Issue 13 stats fixture task", currentAttempt: 1, maxAttempts: 99, attemptCount: 1, stepCount: 2, outputCount: 2, terminalUnread: false, summaryOnly: true, lastJudge: { decision: "complete", confidence: 1, reason: "fixture", source: "fixture" }, timing: { durationMs: 420000, totalElapsedMs: 480000 }, }; return { ok: true, queue: { activeTaskId: "", activeTaskIds: [], total: 1, unreadTerminal: 0, counts: { succeeded: 1, queued: 0, running: 0, retry_wait: 0, judging: 0 }, queues: [{ id: "issue13-stats-fixture", name: "Issue 13 Stats Fixture", total: 1, counts: { succeeded: 1 } }], mainProviderId: "D601-dev", defaultProviderId: "D601-dev", defaultWorkdir: "/workspace-dev", defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" }, codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"], executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }], executionModes: [{ id: "default", label: "默认容器/本机" }], }, statistics, tasks: [task], selected: null, pagination: { returned: 1, total: 1, hasMore: false }, }; } async function runCodeQueueStatsFixture(page: Page, frontendUrl: string, mode: "visible" | "degraded"): Promise { const taskId = `codex_issue13_stats_${mode}`; const visibleStats = { generatedAt: "2026-05-20T05:20:00.000Z", timezone: "Asia/Shanghai", source: "fixture-dailyBuckets", range: { startDate: "2026-05-18", endDate: "2026-05-20" }, totals: { executedTasks: 9, completedTasks: 7, retryAttempts: 3, avgDurationMs: 420000, durationSamples: 7 }, dailyBuckets: [ { date: "2026-05-18", tasks: 2, terminalCount: 2, retries: 1, averageDurationMs: 300000, samples: 2 }, { date: "2026-05-19", tasks: 3, terminalCount: 2, retries: 0, averageDurationMs: 360000, samples: 2 }, { date: "2026-05-20", tasks: 4, terminalCount: 3, retries: 2, averageDurationMs: 540000, samples: 3 }, ], }; const degradedStats = { generatedAt: "2026-05-20T05:20:00.000Z", timezone: "Asia/Shanghai", source: "fixture-stats-api", degraded: true, unavailable: true, reason: "fixture stats API timeout: database projection unavailable", range: { startDate: "2026-05-18", endDate: "2026-05-20" }, totals: { executedTasks: 0, completedTasks: 0, retryAttempts: 0, avgDurationMs: null, durationSamples: 0 }, daily: [], }; const statistics = mode === "visible" ? visibleStats : degradedStats; const overview = codeQueueStatsFixtureOverview(statistics, taskId); const routePattern = "**/api/microservices/code-queue/proxy/api/**"; const requests: string[] = []; const handler = async (route: any, request: any): Promise => { if (request.method() !== "GET") { await route.continue(); return; } const url = new URL(request.url()); const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, ""); requests.push(`${path}${url.search}`); if (path === "/api/tasks/overview") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) }); return; } if (path === "/api/tasks/stats") { if (mode === "degraded") { await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ ok: false, error: degradedStats.reason, statistics: degradedStats }) }); } else { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, statistics, queue: overview.queue }) }); } return; } if (path === "/api/workdirs") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) }); return; } if (path === "/api/queues") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) }); return; } if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary: { ...overview.tasks[0], finalResponse: "Fixture complete", attempts: [], prompt: {}, execution: { durationMs: 420000, traceLineCount: 2, stepCount: 2 } } }) }); return; } await route.continue(); }; await page.route(routePattern, handler); try { await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000, state: "attached" }); await page.waitForSelector('[data-testid="codex-stats-chart"]', { timeout: 15000 }); if (mode === "visible") { await page.waitForSelector('[data-testid="codex-stats-day-2026-05-20"]', { timeout: 15000 }); } else { await page.waitForSelector('[data-testid="codex-stats-diagnostic"]', { timeout: 15000 }); } const metrics = await page.evaluate(() => { const hero = document.querySelector('[data-testid="codex-stats-panel"]') as HTMLElement | null; const chart = document.querySelector('[data-testid="codex-stats-chart"]') as HTMLElement | null; const list = document.querySelector('[data-testid="codex-stats-daily-list"]') as HTMLElement | null; const diagnostic = document.querySelector('[data-testid="codex-stats-diagnostic"]') as HTMLElement | null; const statsPanel = document.querySelector(".codex-stats-panel") as HTMLElement | null; const svg = chart?.querySelector("svg") as SVGElement | null; const chartRect = chart?.getBoundingClientRect(); return { heroText: hero?.innerText || hero?.textContent || "", state: hero?.getAttribute("data-stats-state") || "", degraded: hero?.getAttribute("data-stats-degraded") || "", chartVisible: Boolean(chart), chartEmpty: chart?.getAttribute("data-empty") || "", chartHeight: Math.round(chartRect?.height || 0), polylineCount: svg?.querySelectorAll("polyline.stat-line").length ?? 0, pointCount: svg?.querySelectorAll(".stat-point-group").length ?? 0, dailyRows: list?.querySelectorAll('[data-testid^="codex-stats-day-"]:not([data-testid="codex-stats-day-empty"])').length ?? 0, dailyText: list?.textContent || "", diagnosticText: diagnostic?.textContent || "", emptyText: statsPanel?.querySelector(".empty-state")?.textContent || "", summaryText: statsPanel?.textContent || "", }; }); return { checked: true, mode, taskId, fixture: { statsKeys: Object.keys(statistics), bucketKey: mode === "visible" ? "dailyBuckets" : "daily", totals: statistics.totals, }, requests, ...metrics, ok: mode === "visible" ? metrics.state === "ready" && metrics.chartVisible === true && metrics.chartEmpty === "false" && metrics.dailyRows >= 3 && metrics.pointCount >= 9 && metrics.heroText.includes("9 tasks / 3 retries") && metrics.dailyText.includes("4 exec") && !metrics.emptyText.includes("暂无统计") : metrics.state === "unavailable" && metrics.degraded === "true" && metrics.chartVisible === true && metrics.chartHeight >= 180 && metrics.diagnosticText.includes("fixture stats API timeout") && metrics.summaryText.includes("统计不可用") && metrics.chartEmpty === "true" && metrics.heroText.includes("0 tasks / 0 retries"), }; } finally { await page.unroute(routePattern, handler).catch(() => undefined); } } function codeQueueSplitBrainFixtureOverview(mode: "live" | "stale"): any { const taskId = mode === "live" ? "codex_issue14_split_brain_live" : "codex_issue14_split_brain_stale"; const now = "2026-05-20T06:00:00.000Z"; const freshAt = "2026-05-20T05:59:52.000Z"; const staleAt = "2026-05-20T05:42:00.000Z"; const activeAt = mode === "live" ? freshAt : staleAt; const diagnostics = mode === "live" ? { state: "split-brain", health: "split-brain", degraded: true, splitBrain: true, splitBrainLive: true, effectiveLiveness: "live", recommendedAction: "continue-supervision", livenessSummary: "PostgreSQL and the local control-plane view are split, but scheduler-owned heartbeat and trace are fresh; treat the task as live and continue supervision.", executionStateSource: "postgres-control-plane", controlPlane: "master-code-queue-mgr", databaseActiveTaskIds: [taskId], databaseActiveTaskCount: 1, schedulerActiveTaskIds: [], schedulerActiveTaskCount: 0, schedulerActiveRunSlotCount: 0, schedulerActiveQueueIds: [], schedulerProcessingQueueIds: [], schedulerOrphanedActiveTaskIds: [taskId], schedulerOrphanedActiveTaskCount: 1, activeHeartbeatTaskIds: [taskId], activeHeartbeatCount: 1, heartbeatFreshTaskIds: [taskId], heartbeatExpiredTaskIds: [], heartbeatMissingTaskIds: [], staleRecoveryCandidateTaskIds: [], heartbeatRiskTaskIds: [], traceGapTaskIds: [], traceGapNotStaleTaskIds: [taskId], schedulerHeartbeatStaleMs: 300000, now, lastSchedulerHeartbeatAt: freshAt, lastObservedAgentEventAt: freshAt, lastPersistedTraceAt: freshAt, oaPublisher: null, reasons: ["postgres control-plane has database-active tasks while its local active slots are empty, but scheduler heartbeat is fresh"], guidance: ["Continue supervision; do not restart solely because master activeRunSlotCount is zero."], } : { state: "split-brain", health: "split-brain", degraded: true, splitBrain: true, splitBrainLive: false, effectiveLiveness: "at-risk", recommendedAction: "investigate-heartbeat-risk", livenessSummary: "Heartbeat is expired for a database-active task; investigate before assuming the task is still live.", executionStateSource: "postgres-control-plane", controlPlane: "master-code-queue-mgr", databaseActiveTaskIds: [taskId], databaseActiveTaskCount: 1, schedulerActiveTaskIds: [], schedulerActiveTaskCount: 0, schedulerActiveRunSlotCount: 0, schedulerActiveQueueIds: [], schedulerProcessingQueueIds: [], schedulerOrphanedActiveTaskIds: [taskId], schedulerOrphanedActiveTaskCount: 1, activeHeartbeatTaskIds: [taskId], activeHeartbeatCount: 1, heartbeatFreshTaskIds: [], heartbeatExpiredTaskIds: [taskId], heartbeatMissingTaskIds: [], staleRecoveryCandidateTaskIds: [taskId], heartbeatRiskTaskIds: [taskId], traceGapTaskIds: [taskId], traceGapNotStaleTaskIds: [], schedulerHeartbeatStaleMs: 300000, now, lastSchedulerHeartbeatAt: staleAt, lastObservedAgentEventAt: staleAt, lastPersistedTraceAt: staleAt, oaPublisher: null, reasons: ["owner heartbeat is expired and scheduler has no local active run for at least one database-active task"], guidance: ["Investigate heartbeat risk before retry or restart."], }; const task = { id: taskId, queueId: "issue14-liveness-fixture", queueEnteredAt: "2026-05-20T05:50:00.000Z", displayPrompt: `Issue #14 ${mode} split-brain fixture`, basePrompt: `Issue #14 ${mode} split-brain fixture`, displayPromptPreview: `Issue #14 ${mode} split-brain fixture`, providerId: "D601-dev", executionMode: "default", cwd: "/workspace-dev", model: "gpt-5.5", maxAttempts: 99, status: "running", createdAt: "2026-05-20T05:50:00.000Z", updatedAt: activeAt, startedAt: "2026-05-20T05:50:10.000Z", finishedAt: null, currentAttempt: 1, currentMode: "initial", codexThreadId: "thread_issue14_fixture", activeTurnId: "turn_issue14_fixture", finalResponse: "", stepCount: mode === "live" ? 4 : 0, llmStepCount: mode === "live" ? 2 : 0, outputCount: mode === "live" ? 8 : 1, eventCount: 1, attemptCount: 0, attempts: [], summaryOnly: true, promptEditable: false, schedulerHeartbeat: { taskId, queueId: "issue14-liveness-fixture", attempt: 1, activeTurnId: "turn_issue14_fixture", codexThreadId: "thread_issue14_fixture", owner: "D601-dev", schedulerInstance: "code-queue-scheduler-fixture", executionPlane: "scheduler-execution-plane", agentPort: "codex", status: "running", lastLocalHeartbeatAt: activeAt, lastObservedAgentEventAt: activeAt, lastPersistedTraceAt: activeAt, outputMaxSeq: mode === "live" ? 8 : 1, source: "scheduler", }, timing: { durationMs: 600000, totalElapsedMs: 600000 }, }; const queue = { activeTaskId: taskId, activeTaskIds: [taskId], total: 1, unreadTerminal: 0, counts: { running: 1, queued: 0, retry_wait: 0, judging: 0 }, queues: [{ id: "issue14-liveness-fixture", name: "Issue 14 Liveness Fixture", total: 1, activeTaskId: taskId, counts: { running: 1 } }], mainProviderId: "D601-dev", defaultProviderId: "D601-dev", defaultWorkdir: "/workspace-dev", defaultWorkdirByProvider: { "D601-dev": "/workspace-dev" }, codeModels: ["gpt-5.5", "gpt-5.4-mini", "gpt-5.4"], executionProviders: [{ id: "D601-dev", label: "D601 dev", defaultWorkdir: "/workspace-dev" }], executionModes: [{ id: "default", label: "默认容器/本机" }], executionDiagnostics: diagnostics, }; return { ok: true, queue, statistics: { timezone: "Asia/Shanghai", range: { startDate: "2026-05-20", endDate: "2026-05-20" }, totals: {}, daily: [] }, tasks: [task], pagination: { returned: 1, total: 1, hasMore: false }, selected: { task, transcript: [], hasMore: false, preview: true }, }; } async function runCodeQueueSplitBrainFixture(page: Page, frontendUrl: string, mode: "live" | "stale"): Promise { const overview = codeQueueSplitBrainFixtureOverview(mode); const taskId = String(overview.tasks[0]?.id || ""); const diagnostics = overview.queue.executionDiagnostics; const routePattern = "**/api/microservices/code-queue/proxy/api/**"; const requests: string[] = []; const handler = async (route: any, request: any): Promise => { if (request.method() !== "GET") { await route.continue(); return; } const url = new URL(request.url()); const path = url.pathname.replace(/^\/api\/microservices\/code-queue\/proxy/u, ""); requests.push(`${path}${url.search}`); if (path === "/api/tasks/overview") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(overview) }); return; } if (path === "/api/workdirs") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, workdirs: [{ providerId: "D601-dev", executionMode: "default", path: "/workspace-dev", source: "fixture" }] }) }); return; } if (path === "/api/queues") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, queues: overview.queue.queues, summary: overview.queue }) }); return; } if (path === `/api/tasks/${encodeURIComponent(taskId)}/trace-summary` || path === `/api/tasks/${taskId}/trace-summary`) { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true, summary: { ...overview.tasks[0], finalResponse: "", attempts: [], prompt: {}, execution: { durationMs: 600000, traceLineCount: 2, stepCount: mode === "live" ? 2 : 0 } } }) }); return; } await route.continue(); }; await page.route(routePattern, handler); try { await page.goto(`${frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000, state: "attached" }); await page.waitForSelector('[data-testid="codex-liveness-diagnostics"]', { timeout: 15000 }); await page.waitForFunction((expectedAction) => { const panel = document.querySelector(".codex-liveness-panel") as HTMLElement | null; return Boolean(panel && (panel.textContent || "").includes(String(expectedAction))); }, diagnostics.recommendedAction, { timeout: 15000 }); const metrics = await page.evaluate(() => { const panel = document.querySelector(".codex-liveness-panel") as HTMLElement | null; const grid = document.querySelector('[data-testid="codex-liveness-diagnostics"]') as HTMLElement | null; const metricRows = Array.from(grid?.querySelectorAll(".codex-liveness-metric") || []) as HTMLElement[]; const metric = (label: string): any => { const row = metricRows.find((item) => (item.querySelector("span")?.textContent || "").trim() === label) || null; return { className: String(row?.className || ""), label: row?.querySelector("span")?.textContent || "", value: row?.querySelector("strong")?.textContent || "", hint: row?.querySelector("code")?.textContent || "", }; }; const chips = Array.from(panel?.querySelectorAll(".codex-trace-status-chip.liveness") || []) as HTMLElement[]; const advisory = panel?.querySelector(".codex-liveness-advisory") as HTMLElement | null; const text = panel?.innerText || panel?.textContent || ""; return { panelText: text, health: metric("健康状态"), effective: metric("Effective liveness"), heartbeatRisk: metric("Heartbeat risk"), staleCandidates: metric("Stale candidates"), livenessChipClasses: chips.map((chip) => String(chip.className || "")), advisoryClass: String(advisory?.className || ""), advisoryText: advisory?.textContent || "", failedMetricCount: metricRows.filter((row) => row.classList.contains("failed")).length, healthFailed: metric("健康状态").className.includes("failed"), effectiveFailed: metric("Effective liveness").className.includes("failed"), warns: metricRows.filter((row) => row.classList.contains("warn") || row.classList.contains("degraded-live")).length, }; }); const ok = mode === "live" ? metrics.health.value === "split-brain live" && metrics.health.className.includes("degraded-live") && !metrics.health.className.includes("failed") && metrics.effective.value === "live" && metrics.heartbeatRisk.value === "0" && metrics.failedMetricCount === 0 && metrics.panelText.includes("continue-supervision") && metrics.panelText.includes("控制面/执行面观测分裂") && metrics.panelText.includes("heartbeat/trace 新鲜") && !/任务死亡|dead task|task death|必须重启|must restart/i.test(metrics.panelText) : metrics.health.value === "split-brain" && metrics.health.className.includes("failed") && metrics.effective.value === "at-risk" && metrics.heartbeatRisk.value === "1" && metrics.heartbeatRisk.className.includes("failed") && metrics.staleCandidates.value === "1" && metrics.panelText.includes("investigate-heartbeat-risk") && metrics.panelText.includes(taskId) && metrics.failedMetricCount >= 3; return { checked: true, mode, taskId, fixtureDiagnostics: { state: diagnostics.state, splitBrainLive: diagnostics.splitBrainLive, effectiveLiveness: diagnostics.effectiveLiveness, recommendedAction: diagnostics.recommendedAction, heartbeatFreshTaskIds: diagnostics.heartbeatFreshTaskIds, heartbeatRiskTaskIds: diagnostics.heartbeatRiskTaskIds, traceGapNotStaleTaskIds: diagnostics.traceGapNotStaleTaskIds, staleRecoveryCandidateTaskIds: diagnostics.staleRecoveryCandidateTaskIds, }, requests, ...metrics, ok, }; } finally { await page.unroute(routePattern, handler).catch(() => undefined); } } function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } { const result = runCommand([ "docker", "exec", "unidesk-database", "psql", "-U", config.database.user, "-d", config.database.name, "-v", "ON_ERROR_STOP=1", "-tA", "-c", sql, ], repoRoot); return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode }; } function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown { const method = init?.method ?? "GET"; const body = init?.body === undefined ? "" : JSON.stringify(init.body); const url = `http://127.0.0.1:8080${path}`; const script = [ "set -euo pipefail", "if command -v backend-core >/dev/null 2>&1; then", ` exec backend-core --fetch-json ${shellQuote(url)} --method ${shellQuote(method)}${body.length > 0 ? ` --body-json ${shellQuote(body)}` : ""}`, "fi", `url=${shellQuote(url)}`, `method=${shellQuote(method)}`, `body=${shellQuote(body)}`, "export url method body", "bun -e 'const url=process.env.url; const method=process.env.method; const body=process.env.body; fetch(url,{method,body:body?body:undefined,headers:body?{\"content-type\":\"application/json\"}:undefined}).then(async r=>{const text=await r.text(); console.log(JSON.stringify({ok:r.ok,status:r.status,body:text?JSON.parse(text):null})); process.exit(r.ok?0:1);}).catch(e=>{console.error(e); process.exit(1)})'", ].join("\n"); const command = ["docker", "exec", "unidesk-backend-core", "sh", "-lc", script]; const result = runCommand(command, repoRoot); if (result.exitCode !== 0) return { ok: false, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) }; try { return JSON.parse(result.stdout.trim()) as unknown; } catch { return { ok: true, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) }; } } function createPublicFrontendApiClient(config: UniDeskConfig, baseUrl: string): E2EApiClient { const loginBody = JSON.stringify({ username: process.env.UNIDESK_E2E_AUTH_USERNAME || config.auth.username, password: process.env.UNIDESK_E2E_AUTH_PASSWORD || config.auth.password, }); const login = runCommand([ "curl", "-sS", "-i", "-X", "POST", "-H", "content-type: application/json", "--data-binary", loginBody, `${baseUrl}/login`, ], repoRoot, { timeoutMs: 15_000 }); if (login.exitCode !== 0) { return { getJson: () => ({ ok: false, exitCode: login.exitCode, stdout: login.stdout.slice(-1200), stderr: login.stderr.slice(-1200), }), }; } const cookie = login.stdout .split(/\r?\n/u) .map((line) => line.match(/^set-cookie:\s*([^;]+)/iu)?.[1] || "") .filter(Boolean) .join("; "); return { getJson(path: string, init?: { method?: string; body?: unknown }): unknown { const method = init?.method ?? "GET"; const body = init?.body === undefined ? "" : JSON.stringify(init.body); const command = [ "curl", "-sS", "-w", "\n%{http_code}", "-X", method, "-H", "accept: application/json", ]; if (cookie.length > 0) command.push("-H", `cookie: ${cookie}`); if (body.length > 0) command.push("-H", "content-type: application/json", "--data-binary", body); command.push(new URL(path, baseUrl).toString()); const result = runCommand(command, repoRoot, { timeoutMs: 30_000 }); if (result.exitCode !== 0) return { ok: false, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) }; const lastNewline = result.stdout.lastIndexOf("\n"); const responseText = lastNewline >= 0 ? result.stdout.slice(0, lastNewline) : result.stdout; const status = Number(lastNewline >= 0 ? result.stdout.slice(lastNewline + 1).trim() : 0); let responseBody: unknown = null; try { responseBody = responseText.length > 0 ? JSON.parse(responseText) : null; } catch { responseBody = { text: responseText }; } return { ok: status >= 200 && status < 300, status, body: responseBody }; }, }; } function shellQuote(value: string): string { return `'${value.replace(/'/g, "'\\''")}'`; } function pipelineDetailHasLiveObservation(detail: unknown): boolean { const body = (detail as { body?: { procedureRuns?: unknown[]; controlEvents?: unknown[] } }).body; const text = JSON.stringify(body ?? {}); const procedureRuns = Array.isArray(body?.procedureRuns) ? body.procedureRuns : []; const hasObservation = text.includes("node-long-running-observation"); const hasRunning = procedureRuns.some((procedure: any) => String(procedure?.status?.status || procedure?.artifact?.status || procedure?.status || "").toLowerCase() === "running"); return hasObservation && hasRunning; } function findPipelineLiveObservationCandidate(): { ok: boolean; candidate: { pipelineId: string; runId: string } | null; latest?: unknown } { const snapshot = dockerCoreJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:2,runs:30"); const runs = (snapshot as { body?: { runs?: Array<{ runId?: string; pipelineId?: string }> } }).body?.runs ?? []; let latest: unknown = snapshot; for (const run of runs) { const runId = String(run?.runId || ""); if (!runId) continue; const detail = dockerCoreJson(`/api/microservices/pipeline/proxy/api/node-control/runs/${encodeURIComponent(runId)}?tail=180&view=timeline&_=${Date.now()}`); latest = detail; if (pipelineDetailHasLiveObservation(detail)) { return { ok: true, candidate: { pipelineId: String(run?.pipelineId || ""), runId }, latest: detail }; } } return { ok: false, candidate: null, latest }; } function startPipelineLiveObservationRun(): { ok: boolean; runId: string; command: string[]; exitCode: number | null; stdout: string; stderr: string } { const runId = `e2e_observe_live_${Date.now()}`; const task = "UniDesk E2E live Gantt verification: keep a real node running long enough to receive OA long-running observation events."; const startCommand = [ "npx tsx scripts/pipeline-cli.ts pipeline start", "--id monitor-management-behavior-test", `--run-id ${shellQuote(runId)}`, "--timeout-ms 1200000", `--task ${shellQuote(task)}`, ].join(" "); const remoteCommand = `cd /home/ubuntu/pipeline && ${startCommand}`; const command = ["bun", "scripts/cli.ts", "ssh", "D601", remoteCommand]; const result = runCommand(command, repoRoot); return { ok: result.exitCode === 0, runId, command, exitCode: result.exitCode, stdout: result.stdout.slice(-4000), stderr: result.stderr.slice(-4000), }; } async function ensurePipelineLiveObservationCandidate(): Promise { const existing = findPipelineLiveObservationCandidate(); if (existing.candidate) return { ...existing, ok: true, reused: true }; const started = startPipelineLiveObservationRun(); if (!started.ok) return { ok: false, stage: "start", started, existing }; const startedAt = Date.now(); let latest: unknown = null; while (Date.now() - startedAt < 240_000) { const candidate = findPipelineLiveObservationCandidate(); latest = candidate.latest; if (candidate.candidate) return { ...candidate, ok: true, reused: false, started }; await Bun.sleep(10_000); } return { ok: false, stage: "wait-live-observation", timeoutMs: 240_000, started, latest }; } async function waitForTaskStatus(taskId: string, expected: string, timeoutMs = 10_000): Promise { const started = Date.now(); let latest: unknown = null; while (Date.now() - started < timeoutMs) { latest = dockerCoreJson("/api/tasks?limit=40"); const tasks = (latest as { body?: { tasks?: Array<{ id?: string; status?: string; result?: unknown }> } }).body?.tasks ?? []; const task = tasks.find((item) => item.id === taskId); if (task?.status === expected) return { ok: true, task }; if (task?.status === "failed") return { ok: false, task }; await Bun.sleep(500); } return { ok: false, timeoutMs, latest }; } function dockerPortSummary(): unknown { const result = runCommand([ "docker", "ps", "--filter", "label=com.docker.compose.project=unidesk", "--filter", "label=com.docker.compose.oneoff=False", "--format", "{{.Names}}\t{{.Ports}}", ], repoRoot); return { ok: result.exitCode === 0, exitCode: result.exitCode, rows: result.stdout.trim().split("\n").filter(Boolean).map((line) => { const [name, ports = ""] = line.split("\t"); return { name, ports }; }), stderr: result.stderr.trim(), }; } function dockerUserPortRestriction(containerPort: number, allowedSourceCidrs: string[] = []): unknown { const result = runCommand(["iptables", "-S", "DOCKER-USER"], repoRoot); const lines = result.stdout.split("\n").map((line) => line.trim()).filter(Boolean); const dportPattern = new RegExp(`--dport\\s+${containerPort}\\b`); const portLines = lines.filter((line) => dportPattern.test(line)); const acceptSources = portLines .filter((line) => line.includes("-j ACCEPT")) .map((line) => line.match(/(?:^|\s)-s\s+(\S+)/)?.[1] ?? "") .filter((source) => source.length > 0); const hasDrop = portLines.some((line) => line.includes("-j DROP")); const requiredSourcesPresent = allowedSourceCidrs.length === 0 ? acceptSources.length > 0 : allowedSourceCidrs.every((source) => acceptSources.includes(source)); return { ok: result.exitCode === 0 && hasDrop && requiredSourcesPresent, exitCode: result.exitCode, containerPort, acceptSources, allowedSourceCidrs, requiredSourcesPresent, hasDrop, lines: portLines, stderr: result.stderr.slice(-1200), }; } function publicProbeBlockedOrRestricted(probe: unknown, restriction: unknown): boolean { return (probe as { reachable?: boolean }).reachable === false || (restriction as { ok?: boolean }).ok === true; } function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown { const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string; restartPolicy?: string; pidMode?: string }> } }> } }; const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId); return { ok: response.ok, status: response.status, providerId, nodeStatus: item?.nodeStatus, updatedAt: item?.updatedAt, dockerStatus: item?.dockerStatus === undefined ? null : { ok: item.dockerStatus.ok, socketPresent: item.dockerStatus.socketPresent, collectedAt: item.dockerStatus.collectedAt, counts: item.dockerStatus.counts, daemon: item.dockerStatus.daemon, containersPreview: (item.dockerStatus.containers ?? []).slice(0, 8).map((container) => ({ id: container.id, name: container.name, image: container.image, state: container.state, status: container.status, ports: container.ports, restartPolicy: container.restartPolicy, pidMode: container.pidMode, })), }, }; } function providerGatewayRuntimeGuardDetail(dockerStatus: unknown): { ok: boolean; gateways: unknown[]; violations: unknown[] } { const response = dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; nodeStatus?: string; dockerStatus?: { containers?: Array<{ id?: string; name?: string; image?: string; state?: string; restartPolicy?: string; pidMode?: string; composeService?: string; composeProject?: string; }>; }; }>; }; }; const gateways = (response.body?.dockerStatuses ?? []).flatMap((entry) => (entry.dockerStatus?.containers ?? []) .filter((container) => String(container.name || "").includes("provider-gateway") || container.composeService === "provider-gateway") .map((container) => ({ providerId: entry.providerId, nodeStatus: entry.nodeStatus, id: container.id, name: container.name, image: container.image, state: container.state, restartPolicy: container.restartPolicy, pidMode: container.pidMode, composeProject: container.composeProject, composeService: container.composeService, })) ); const violations = gateways.filter((container) => (container as { state?: unknown }).state === "running" && ((container as { restartPolicy?: unknown }).restartPolicy !== "always" || (container as { pidMode?: unknown }).pidMode !== "host") ); return { ok: gateways.length > 0 && violations.length === 0, gateways, violations }; } function systemStatusCheckDetail(systemStatus: unknown, providerId: string): unknown { const response = systemStatus as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; current?: { ok?: boolean; collectedAt?: string; cpu?: unknown; memory?: unknown; disk?: unknown }; history?: unknown[] }> } }; const item = response.body?.systemStatuses?.find((entry) => entry.providerId === providerId); return { ok: response.ok, status: response.status, providerId, nodeStatus: item?.nodeStatus, updatedAt: item?.updatedAt, current: item?.current === undefined ? null : { ok: item.current.ok, collectedAt: item.current.collectedAt, cpu: item.current.cpu, memory: item.current.memory, disk: item.current.disk, }, historyCount: item?.history?.length ?? 0, historyPreview: (item?.history ?? []).slice(-8), }; } async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise { const portSummary = dockerPortSummary() as { rows?: Array<{ name: string; ports: string }> }; const portsText = (portSummary.rows ?? []).map((row) => `${row.name} ${row.ports}`).join("\n"); const corePublic = await fetchProbe(`${urls.blockedCoreUrl}/health`, 2500); const databasePublic = await tcpProbe(urls.blockedDatabaseHost, urls.blockedDatabasePort); const allowedSourceCidrs = config.network.restrictedHostAccess?.allowedSourceCidrs ?? []; const databaseRestriction = dockerUserPortRestriction(config.network.database.containerPort, allowedSourceCidrs); const findjobPublic = await fetchProbe(`http://${config.network.publicHost}:3254/api/health`, 2500); const metNonlinearPublic = await fetchProbe(`http://${config.network.publicHost}:3288/health`, 2500); const claudeqqPublic = await fetchProbe(`http://${config.network.publicHost}:3290/health`, 2500); const todoNotePublic = await fetchProbe(`http://${config.network.publicHost}:4211/api/health`, 2500); const codeQueuePublic = await fetchProbe(`http://${config.network.publicHost}:14222/health`, 2500); const oaEventFlowPublic = await fetchProbe(`http://${config.network.publicHost}:4255/health`, 2500); const oaEventFlowRestriction = dockerUserPortRestriction(4255, allowedSourceCidrs); const decisionCenterPublic = await fetchProbe(`http://${config.network.publicHost}:4277/health`, 2500); const filebrowserPublic = await fetchProbe(`http://${config.network.publicHost}:4251/health`, 2500); addSelectedCheck(checks, options, "network:only-frontend-provider-ports", !portsText.includes(`:${config.network.core.port}->`) && !portsText.includes(":14222->"), portSummary); addSelectedCheck(checks, options, "network:core-public-blocked", (corePublic as { reachable?: boolean }).reachable === false, corePublic); addSelectedCheck(checks, options, "network:database-public-blocked", publicProbeBlockedOrRestricted(databasePublic, databaseRestriction), { publicProbe: databasePublic, restriction: databaseRestriction }); addSelectedCheck(checks, options, "network:findjob-public-blocked", (findjobPublic as { reachable?: boolean }).reachable === false, findjobPublic); addSelectedCheck(checks, options, "network:met-nonlinear-public-blocked", (metNonlinearPublic as { reachable?: boolean }).reachable === false, metNonlinearPublic); addSelectedCheck(checks, options, "network:claudeqq-public-blocked", (claudeqqPublic as { reachable?: boolean }).reachable === false, claudeqqPublic); addSelectedCheck(checks, options, "network:todo-note-public-blocked", (todoNotePublic as { reachable?: boolean }).reachable === false, todoNotePublic); addSelectedCheck(checks, options, "network:code-queue-public-blocked", (codeQueuePublic as { reachable?: boolean }).reachable === false, codeQueuePublic); addSelectedCheck(checks, options, "network:oa-event-flow-public-blocked", publicProbeBlockedOrRestricted(oaEventFlowPublic, oaEventFlowRestriction), { publicProbe: oaEventFlowPublic, restriction: oaEventFlowRestriction }); addSelectedCheck(checks, options, "network:decision-center-public-blocked", (decisionCenterPublic as { reachable?: boolean }).reachable === false, decisionCenterPublic); addSelectedCheck(checks, options, "network:filebrowser-public-blocked", (filebrowserPublic as { reachable?: boolean }).reachable === false, filebrowserPublic); } async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise { const apiClient = isDevFrontendTarget() ? createPublicFrontendApiClient(config, urls.frontendUrl) : { getJson: dockerCoreJson }; const coreOverview = apiClient.getJson("/api/overview"); const corePerformance = apiClient.getJson("/api/performance"); const coreNodes = apiClient.getJson("/api/nodes"); const systemStatus = apiClient.getJson("/api/nodes/system-status?limit=24"); const dockerStatus = apiClient.getJson("/api/nodes/docker-status"); const microservices = apiClient.getJson("/api/microservices"); const findjobStatus = apiClient.getJson("/api/microservices/findjob/status"); const findjobHealth = apiClient.getJson("/api/microservices/findjob/health"); const findjobSummary = apiClient.getJson("/api/microservices/findjob/proxy/api/summary"); const findjobJobsPreview = apiClient.getJson("/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5"); const pipelineStatus = apiClient.getJson("/api/microservices/pipeline/status"); const pipelineHealth = apiClient.getJson("/api/microservices/pipeline/health"); const pipelineSnapshot = apiClient.getJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3"); const pipelineOaEventFlow = apiClient.getJson("/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics"); const metNonlinearStatus = apiClient.getJson("/api/microservices/met-nonlinear/status"); const metNonlinearHealth = apiClient.getJson("/api/microservices/met-nonlinear/health"); const metNonlinearQueue = apiClient.getJson("/api/microservices/met-nonlinear/proxy/api/queue?__unideskArrayLimit=jobs:10"); const metNonlinearProjects = apiClient.getJson("/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=20&__unideskArrayLimit=projects:20"); const metNonlinearImages = apiClient.getJson("/api/microservices/met-nonlinear/proxy/api/images"); const claudeqqStatus = apiClient.getJson("/api/microservices/claudeqq/status"); const claudeqqHealth = apiClient.getJson("/api/microservices/claudeqq/health"); const claudeqqNapcatLogin = apiClient.getJson("/api/microservices/claudeqq/proxy/api/napcat/login"); const claudeqqEvents = apiClient.getJson("/api/microservices/claudeqq/proxy/api/events/recent?limit=5"); const claudeqqSubscriptions = apiClient.getJson("/api/microservices/claudeqq/proxy/api/events/subscriptions"); const todoNoteStatus = apiClient.getJson("/api/microservices/todo-note/status"); const todoNoteHealth = apiClient.getJson("/api/microservices/todo-note/health"); const todoNoteInstances = apiClient.getJson("/api/microservices/todo-note/proxy/api/instances"); const oaEventFlowStatus = apiClient.getJson("/api/microservices/oa-event-flow/status"); const oaEventFlowHealth = apiClient.getJson("/api/microservices/oa-event-flow/health"); const oaEventFlowDiagnostics = apiClient.getJson("/api/microservices/oa-event-flow/proxy/api/diagnostics"); const oaEventFlowEvents = apiClient.getJson("/api/microservices/oa-event-flow/proxy/api/events?limit=10"); const oaEventFlowPipelineEvents = apiClient.getJson("/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline&limit=10"); const oaEventFlowStats = apiClient.getJson("/api/microservices/oa-event-flow/proxy/api/stats/trace?limit=10"); const k3sctlStatus = apiClient.getJson("/api/microservices/k3sctl-adapter/status"); const k3sctlControlPlane = apiClient.getJson("/api/microservices/k3sctl-adapter/proxy/api/control-plane"); const codeQueueStatus = apiClient.getJson("/api/microservices/code-queue/status"); const codeQueueHealth = apiClient.getJson("/api/microservices/code-queue/health"); const codeQueueWorkdirs = wantsCheck(options, "microservice:code-queue-workdirs") ? apiClient.getJson("/api/microservices/code-queue/proxy/api/workdirs") : null; const codeQueueTasks = apiClient.getJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId="); const decisionCenterStatus = apiClient.getJson("/api/microservices/decision-center/status"); const decisionCenterHealth = apiClient.getJson("/api/microservices/decision-center/health"); const decisionCenterRecords = apiClient.getJson("/api/microservices/decision-center/proxy/api/records?limit=20"); const decisionCenterRecordMarker = `E2E Decision CRUD ${Date.now()}`; const decisionCenterRecordCreate = wantsCheck(options, "microservice:decision-center-record-crud") ? apiClient.getJson("/api/microservices/decision-center/proxy/api/records", { method: "POST", body: { type: "goal", level: "G0", status: "active", title: decisionCenterRecordMarker, body: "E2E creates, edits and deletes a Decision Center requirement record.", tags: ["e2e", "external-goal"], evidenceLinks: ["https://example.com/unidesk/decision-center-record-crud"], }, }) : null; const decisionCenterRecordId = String((decisionCenterRecordCreate as { body?: { record?: { id?: string } } } | null)?.body?.record?.id || ""); const decisionCenterRecordUpdate = decisionCenterRecordId ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(decisionCenterRecordId)}`, { method: "PUT", body: { status: "blocked", level: "P1", body: "E2E edited this Decision Center requirement record before deletion.", tags: ["e2e", "blocked"], }, }) : { ok: false, error: "missing Decision Center record id", create: decisionCenterRecordCreate }; const decisionCenterRecordRead = decisionCenterRecordId ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(decisionCenterRecordId)}`) : { ok: false, error: "missing Decision Center record id", create: decisionCenterRecordCreate }; const decisionCenterRecordDelete = decisionCenterRecordId ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(decisionCenterRecordId)}`, { method: "DELETE" }) : { ok: false, error: "missing Decision Center record id", create: decisionCenterRecordCreate }; const decisionCenterDiaryDate = "2099-12-31"; const decisionCenterDiaryMarker = `Decision Center diary lifecycle ${Date.now()}`; const decisionCenterDiaryCreate = wantsCheck(options, "microservice:decision-center-diary-lifecycle") ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/diary/entries/${decisionCenterDiaryDate}`, { method: "PUT", body: { title: "E2E Decision Center Diary", body: `# ${decisionCenterDiaryDate}\n${decisionCenterDiaryMarker}\n`, sourceFile: "e2e-decision-center.md", tags: ["e2e", "diary"], }, }) : null; const decisionCenterDiaryEdit = decisionCenterDiaryCreate ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/diary/entries/${decisionCenterDiaryDate}`, { method: "PUT", body: { title: "E2E Decision Center Diary Edited", body: `# ${decisionCenterDiaryDate}\n${decisionCenterDiaryMarker}\nEdited.\n`, sourceFile: "e2e-decision-center.md", tags: ["e2e", "diary", "edited"], }, }) : null; const decisionCenterDiaryRead = decisionCenterDiaryCreate ? apiClient.getJson(`/api/microservices/decision-center/proxy/api/diary/entries/${decisionCenterDiaryDate}`) : null; const filebrowserHealth = apiClient.getJson("/api/microservices/filebrowser/health"); const filebrowserWebui = apiClient.getJson("/api/microservices/filebrowser/proxy/"); const filebrowserD601Health = apiClient.getJson("/api/microservices/filebrowser-d601/health"); const todoE2eName = `E2E Todo ${Date.now()}`; const todoNoteCreate = apiClient.getJson("/api/microservices/todo-note/proxy/api/instances", { method: "POST", body: { name: todoE2eName } }); const todoCreatedId = (todoNoteCreate as { body?: { id?: string } }).body?.id ?? ""; const todoNoteAdd = todoCreatedId ? apiClient.getJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "addTodo", title: "E2E migrated write path" } } }) : { ok: false, error: "missing created todo note id", todoNoteCreate }; const todoAddedItems = (todoNoteAdd as { body?: { todos?: Array<{ id?: string }> } }).body?.todos ?? []; const todoAddedId = todoAddedItems[0]?.id ?? ""; const todoNoteToggle = todoCreatedId && todoAddedId ? apiClient.getJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "toggleTodoCompleted", todoId: todoAddedId } } }) : { ok: false, error: "missing todo id", todoNoteAdd }; const todoNoteUndo = todoCreatedId ? apiClient.getJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/undo`, { method: "POST", body: {} }) : { ok: false, error: "missing created todo note id" }; const todoNoteDelete = todoCreatedId ? apiClient.getJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}`, { method: "DELETE" }) : { ok: false, error: "missing created todo note id" }; const providerIngress = await fetchProbe(urls.providerIngressHealthUrl); const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number; pgdata?: { volumeName?: string; databaseBytes?: number } } }).body; const corePerformanceBody = (corePerformance as { body?: { ok?: boolean; requests?: { componentSummary?: unknown[] }; operations?: { summary?: unknown[] }; database?: { pgdata?: { volumeName?: string }; codeQueueStorage?: { table?: string } } } }).body; const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string; labels?: Record }> } }).body?.nodes ?? []; const mainNode = nodeList.find((node) => node.providerId === config.providerGateway.id); const expectedGatewayVersion = providerGatewayPackageVersion(); const systemStatuses = (systemStatus as { body?: { systemStatuses?: Array<{ providerId?: string; current?: { cpu?: { percent?: number }; memory?: { percent?: number; mode?: string; cacheBytes?: number }; disk?: { percent?: number }; processes?: Array<{ pid?: number; memoryBytes?: number; memoryMode?: string; rssBytes?: number; cpuPercent?: number; command?: string }>; processSummary?: { defaultSort?: string; visible?: number; total?: number; memoryMode?: string } }; history?: unknown[] }> } }).body?.systemStatuses ?? []; const mainSystem = systemStatuses.find((item) => item.providerId === config.providerGateway.id); const mainProcesses = mainSystem?.current?.processes ?? []; const processMemoryBytes = (row: { memoryBytes?: number }): number => Number(row.memoryBytes ?? -1); const processMemoryDescending = mainProcesses.length < 2 || mainProcesses.every((row, index, rows) => index === 0 || processMemoryBytes(rows[index - 1] ?? {}) >= processMemoryBytes(row)); const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? []; const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id); const providerGatewayRuntimeGuard = providerGatewayRuntimeGuardDetail(dockerStatus); addSelectedCheck(checks, options, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview); addSelectedCheck(checks, options, "core:pgdata-usage", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.pgdata?.volumeName === config.database.volume && Number(overviewBody.pgdata.databaseBytes ?? 0) > 0, coreOverview); addSelectedCheck(checks, options, "core:performance-api", (corePerformance as { ok?: boolean }).ok === true && corePerformanceBody?.ok === true && Array.isArray(corePerformanceBody.requests?.componentSummary) && Array.isArray(corePerformanceBody.operations?.summary) && corePerformanceBody.database?.pgdata?.volumeName === config.database.volume && corePerformanceBody.database?.codeQueueStorage?.table === "unidesk_code_queue_tasks", corePerformance); addSelectedCheck(checks, options, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes); addSelectedCheck(checks, options, "provider:gateway-version-label", mainNode?.labels?.providerGatewayVersion === expectedGatewayVersion && mainNode?.labels?.providerGatewayUpgradePolicy === "always-enabled", { providerId: config.providerGateway.id, expectedGatewayVersion, labels: mainNode?.labels ?? null }); addSelectedCheck(checks, options, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && mainSystem.current.memory?.mode === "actual_without_cache" && Number.isFinite(mainSystem.current.memory?.cacheBytes) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id)); addSelectedCheck(checks, options, "provider:process-resource-status", mainProcesses.length > 0 && mainSystem?.current?.processSummary?.defaultSort === "memory_desc" && typeof mainSystem.current.processSummary.memoryMode === "string" && processMemoryDescending && mainProcesses.some((row) => Number.isFinite(row.pid) && Number.isFinite(row.memoryBytes) && typeof row.memoryMode === "string" && Number.isFinite(row.rssBytes) && Number.isFinite(row.cpuPercent) && typeof row.command === "string"), { providerId: config.providerGateway.id, processSummary: mainSystem?.current?.processSummary, sample: mainProcesses.slice(0, 5) }); addSelectedCheck(checks, options, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id)); addSelectedCheck(checks, options, "provider:gateway-restart-policy", providerGatewayRuntimeGuard.ok, providerGatewayRuntimeGuard); const microserviceList = (microservices as { body?: { microservices?: Array<{ id?: string; providerId?: string; backend?: { public?: boolean; proxyMode?: string }; deployment?: { mode?: string }; runtime?: { orchestrator?: string; providerStatus?: string; container?: { name?: string; state?: string } | null } }> } }).body?.microservices ?? []; const findjob = microserviceList.find((service) => service.id === "findjob"); const pipeline = microserviceList.find((service) => service.id === "pipeline"); const metNonlinear = microserviceList.find((service) => service.id === "met-nonlinear"); const claudeqq = microserviceList.find((service) => service.id === "claudeqq"); const todoNote = microserviceList.find((service) => service.id === "todo-note"); const oaEventFlow = microserviceList.find((service) => service.id === "oa-event-flow"); const codeQueue = microserviceList.find((service) => service.id === "code-queue"); const decisionCenter = microserviceList.find((service) => service.id === "decision-center"); const filebrowser = microserviceList.find((service) => service.id === "filebrowser"); const filebrowserD601 = microserviceList.find((service) => service.id === "filebrowser-d601"); const findjobSummaryBody = (findjobSummary as { body?: { totalJobs?: number; prioritizedJobs?: number } }).body; const findjobJobs = (findjobJobsPreview as { body?: { jobs?: unknown[]; _unidesk?: { arrayLimits?: { jobs?: { returnedLength?: number; originalLength?: number } } } } }).body; const todoNoteRows = (todoNoteInstances as { body?: { instances?: Array<{ id?: string; name?: string; todoCount?: number; completedCount?: number }> } }).body?.instances ?? []; const todoNoteNames = todoNoteRows.map((row) => row.name); const pipelineSnapshotBody = (pipelineSnapshot as { body?: { ok?: boolean; registry?: { ok?: boolean; components?: unknown[] }; pipelines?: unknown[]; runs?: unknown[]; _unidesk?: { arrayLimits?: { "registry.components"?: { returnedLength?: number; originalLength?: number }; runs?: { returnedLength?: number; originalLength?: number } } } } }).body; const pipelineOaBody = (pipelineOaEventFlow as { body?: { ok?: boolean; mode?: string; forbiddenResiduals?: unknown[]; runs?: Array<{ runId?: string; nodeFinishedCount?: number; nodeFinishedWithPolicyCount?: number; monitorAuditNodeFinishedCount?: number; noAuditPolicyCount?: number; controlQueuedCount?: number; controlAppliedCount?: number }>; hasNeutralNodeFinishedEvidence?: boolean; hasNoAuditPolicyEvidence?: boolean; hasAuditPolicyEvidence?: boolean } }).body; const metNonlinearHealthBody = (metNonlinearHealth as { body?: { ok?: boolean; targetGpu?: { name?: string; freeRatio?: number } | null; image?: { present?: boolean; image?: string } } }).body; const metNonlinearQueueBody = (metNonlinearQueue as { body?: { ok?: boolean; queue?: { counts?: Record; maxConcurrency?: number; targetGpuName?: string }; jobs?: unknown[] } }).body; const metNonlinearProjectsBody = (metNonlinearProjects as { body?: { ok?: boolean; projects?: unknown[] } }).body; const metNonlinearImagesBody = (metNonlinearImages as { body?: { ok?: boolean; mlImage?: { present?: boolean; image?: string } } }).body; const claudeqqHealthBody = (claudeqqHealth as { body?: { ok?: boolean; service?: string; endpoints?: string[]; subscriptions?: { count?: number; enabled?: number }; napcat?: { containerized?: boolean; connected?: boolean; httpConnected?: boolean; wsConnected?: boolean; loginState?: string; qrcode?: { available?: boolean; dataUrl?: string } } } }).body; const claudeqqNapcatLoginBody = (claudeqqNapcatLogin as { body?: { ok?: boolean; napcat?: { containerized?: boolean; connected?: boolean; httpConnected?: boolean; wsConnected?: boolean; loginState?: string; qrcode?: { available?: boolean; dataUrl?: string } }; login?: { ready?: boolean; state?: string } } }).body; const claudeqqEventsBody = (claudeqqEvents as { body?: { ok?: boolean; events?: unknown[]; count?: number } }).body; const claudeqqSubscriptionsBody = (claudeqqSubscriptions as { body?: { ok?: boolean; subscriptions?: unknown[]; count?: number } }).body; const oaEventFlowHealthBody = (oaEventFlowHealth as { body?: { ok?: boolean; service?: string; databaseReady?: boolean; eventCount?: number; traceStatsCount?: number; latestSequence?: number; pipelineBridge?: { enabled?: boolean; inFlight?: boolean; lastFinishedAt?: string; lastError?: string; insertedCount?: number; duplicateCount?: number } } }).body; const oaEventFlowDiagnosticsBody = (oaEventFlowDiagnostics as { body?: { ok?: boolean; service?: string; databaseReady?: boolean; eventCount?: number; traceStatsCount?: number; latestSequence?: number; eventTypes?: unknown[]; recentEvents?: unknown[]; recentTraceStats?: unknown[]; pipelineBridge?: { enabled?: boolean; lastFinishedAt?: string; lastError?: string; insertedCount?: number; duplicateCount?: number; runs?: Record } } }).body; const oaEventFlowEventsBody = (oaEventFlowEvents as { body?: { ok?: boolean; events?: unknown[]; returned?: number } }).body; const oaEventFlowPipelineEventsBody = (oaEventFlowPipelineEvents as { body?: { ok?: boolean; events?: Array<{ tags?: unknown[]; sourceId?: string; type?: string; payload?: { runId?: string; pipelineId?: string } }>; returned?: number } }).body; const oaEventFlowStatsBody = (oaEventFlowStats as { body?: { ok?: boolean; stats?: unknown[]; returned?: number } }).body; const codeQueueHealthBody = (codeQueueHealth as { body?: { ok?: boolean; egressProxy?: { connected?: boolean }; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record } } }).body; const codeQueueWorkdirsBody = (codeQueueWorkdirs as { body?: { ok?: boolean; workdirs?: Array<{ path?: string; providerId?: string; executionMode?: string }>; defaultProviderId?: string; defaultWorkdir?: string } } | null)?.body; const codeQueueTasksBody = (codeQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record }; tasks?: unknown[] } }).body; const decisionCenterHealthBody = (decisionCenterHealth as { body?: { ok?: boolean; service?: string; storage?: string; schemaReady?: boolean; recordCount?: number; deploy?: { commit?: string } } }).body; const decisionCenterRecordsBody = (decisionCenterRecords as { body?: { ok?: boolean; records?: unknown[]; returned?: number } }).body; const k3sctlControlPlaneBody = (k3sctlControlPlane as { body?: { ok?: boolean; clusterId?: string; noFallback?: boolean; managedServicesHealthy?: boolean; kubeApiProxy?: { mode?: string }; services?: Array<{ id?: string; status?: string; presentNodeIds?: string[]; missingNodeIds?: string[]; topologyComplete?: boolean; servingHealthy?: boolean; active?: { id?: string; healthy?: boolean }; instances?: Array<{ id?: string; healthy?: boolean; proxyMode?: string }>; }>; } }).body; const k3sctlCodeQueueService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue"); const k3sctlClaudeqqService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "claudeqq"); const k3sctlDecisionCenterService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "decision-center"); const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body; const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body; const filebrowserWebuiText = String((filebrowserWebui as { body?: { text?: string } }).body?.text || ""); const firstPipelineRun = Array.isArray(pipelineSnapshotBody?.runs) ? pipelineSnapshotBody.runs[0] as { runId?: string; pipelineId?: string; status?: string; updatedAt?: string } | undefined : undefined; const pipelineSnapshotDetail = { ok: (pipelineSnapshot as { ok?: boolean }).ok, status: (pipelineSnapshot as { status?: number }).status, body: { ok: pipelineSnapshotBody?.ok, registryOk: pipelineSnapshotBody?.registry?.ok, componentPreviewCount: pipelineSnapshotBody?.registry?.components?.length ?? 0, pipelinePreviewCount: pipelineSnapshotBody?.pipelines?.length ?? 0, runPreviewCount: pipelineSnapshotBody?.runs?.length ?? 0, firstRun: firstPipelineRun === undefined ? null : { runId: firstPipelineRun.runId, pipelineId: firstPipelineRun.pipelineId, status: firstPipelineRun.status, updatedAt: firstPipelineRun.updatedAt, }, arrayLimits: pipelineSnapshotBody?._unidesk?.arrayLimits, }, }; const devServiceMode = isDevFrontendTarget(); const codeQueueStatusProviderOk = devServiceMode ? codeQueue?.providerId === "D601-dev" : codeQueue?.providerId === "D601"; const codeQueueStatusProxyOk = devServiceMode ? codeQueue?.backend?.proxyMode === "dev-k3s-direct" : true; const codeQueueHealthOk = devServiceMode ? (codeQueueHealth as { ok?: boolean }).ok === true && codeQueueHealthBody?.ok === true && (codeQueueHealthBody as { service?: string; role?: string }).service === "code-queue" && (codeQueueHealthBody as { role?: string }).role === "scheduler" && codeQueueHealthBody.egressProxy?.connected === true && codeQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codeQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh" : (codeQueueHealth as { ok?: boolean }).ok === true && codeQueueHealthBody?.ok === true && codeQueueHealthBody.egressProxy?.connected === true && codeQueueHealthBody.queue?.defaultModel === "gpt-5.5" && codeQueueHealthBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh"; const expectedCodeQueueWorkdir = devServiceMode ? "/workspace-dev" : "/workspace"; addSelectedCheck(checks, options, "microservice:catalog-findjob", (microservices as { ok?: boolean }).ok === true && findjob?.providerId === "D601" && findjob.backend?.public === false, { microservices }); addSelectedCheck(checks, options, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-control", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-claudeqq", (microservices as { ok?: boolean }).ok === true && claudeqq?.providerId === "D601" && claudeqq.backend?.public === false && claudeqq.backend?.proxyMode === "k3sctl-adapter-http" && claudeqq.deployment?.mode === "k3sctl-managed" && claudeqq.runtime?.orchestrator === "k3sctl" && claudeqq.runtime?.container === null, { microservices }); addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-oa-event-flow", (microservices as { ok?: boolean }).ok === true && oaEventFlow?.providerId === config.providerGateway.id && oaEventFlow.backend?.public === false && oaEventFlow.runtime?.container?.name === "oa-event-flow-backend", { microservices }); addSelectedCheck(checks, options, "microservice:catalog-code-queue", (microservices as { ok?: boolean }).ok === true && codeQueue?.providerId === "D601" && codeQueue.backend?.public === false && codeQueue.backend?.proxyMode === "k3sctl-adapter-http" && codeQueue.deployment?.mode === "k3sctl-managed" && codeQueue.runtime?.orchestrator === "k3sctl" && codeQueue.runtime?.container === null, { microservices }); addSelectedCheck(checks, options, "microservice:catalog-decision-center", (microservices as { ok?: boolean }).ok === true && decisionCenter?.providerId === "D601" && decisionCenter.backend?.public === false && decisionCenter.backend?.proxyMode === "k3sctl-adapter-http" && decisionCenter.deployment?.mode === "k3sctl-managed" && decisionCenter.runtime?.orchestrator === "k3sctl" && decisionCenter.runtime?.container === null, { microservices }); addSelectedCheck(checks, options, "microservice:k3sctl-adapter-status", (k3sctlStatus as { ok?: boolean; body?: { microservice?: { id?: string; providerId?: string } } }).ok === true && (k3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.id === "k3sctl-adapter" && (k3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", k3sctlStatus); addSelectedCheck(checks, options, "microservice:k3sctl-control-plane", (k3sctlControlPlane as { ok?: boolean }).ok === true && k3sctlControlPlaneBody?.ok === true && k3sctlControlPlaneBody.clusterId === "unidesk-k3s" && k3sctlControlPlaneBody.noFallback === true && k3sctlControlPlaneBody.managedServicesHealthy === true && k3sctlControlPlaneBody.kubeApiProxy?.mode === "kubernetes-api-service-proxy" && k3sctlCodeQueueService?.status === "healthy" && k3sctlCodeQueueService?.topologyComplete === true && k3sctlCodeQueueService?.servingHealthy === true && k3sctlCodeQueueService?.active?.id === "D601" && k3sctlCodeQueueService?.active?.healthy === true && k3sctlClaudeqqService?.status === "healthy" && k3sctlClaudeqqService?.topologyComplete === true && k3sctlClaudeqqService?.servingHealthy === true && k3sctlClaudeqqService?.active?.id === "D601" && k3sctlClaudeqqService?.active?.healthy === true && k3sctlDecisionCenterService?.status === "healthy" && k3sctlDecisionCenterService?.topologyComplete === true && k3sctlDecisionCenterService?.servingHealthy === true && k3sctlDecisionCenterService?.active?.id === "D601" && k3sctlDecisionCenterService?.active?.healthy === true && (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601") && (k3sctlCodeQueueService?.missingNodeIds ?? []).length === 0 && (k3sctlCodeQueueService?.instances ?? []).some((instance) => instance.id === "D601" && instance.healthy === true), { ok: (k3sctlControlPlane as { ok?: boolean }).ok, clusterId: k3sctlControlPlaneBody?.clusterId, noFallback: k3sctlControlPlaneBody?.noFallback, kubeApiProxy: k3sctlControlPlaneBody?.kubeApiProxy, service: k3sctlCodeQueueService, claudeqq: k3sctlClaudeqqService, decisionCenter: k3sctlDecisionCenterService, }); addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true && filebrowser?.providerId === "D518" && filebrowser.backend?.public === false && filebrowser.runtime?.container?.name === "unidesk-filebrowser-d518" && filebrowserD601?.providerId === "D601", { filebrowser, filebrowserD601 }); addSelectedCheck(checks, options, "microservice:filebrowser-health", (filebrowserHealth as { ok?: boolean }).ok === true && filebrowserHealthBody?.status === "OK", filebrowserHealth); addSelectedCheck(checks, options, "microservice:filebrowser-webui", (filebrowserWebui as { ok?: boolean; status?: number }).ok === true && (filebrowserWebui as { status?: number }).status === 200 && filebrowserWebuiText.includes("File Browser"), { status: (filebrowserWebui as { status?: number }).status, textPreview: filebrowserWebuiText.slice(0, 600) }); addSelectedCheck(checks, options, "microservice:filebrowser-d601-health", (filebrowserD601Health as { ok?: boolean }).ok === true && filebrowserD601HealthBody?.status === "OK", filebrowserD601Health); addSelectedCheck(checks, options, "microservice:findjob-status", (findjobStatus as { ok?: boolean }).ok === true && (findjobStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", findjobStatus); addSelectedCheck(checks, options, "microservice:findjob-health", (findjobHealth as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (findjobHealth as { body?: { ok?: boolean } }).body?.ok === true, findjobHealth); addSelectedCheck(checks, options, "microservice:findjob-summary", (findjobSummary as { ok?: boolean }).ok === true && Number.isFinite(findjobSummaryBody?.totalJobs) && Number.isFinite(findjobSummaryBody?.prioritizedJobs), findjobSummary); addSelectedCheck(checks, options, "microservice:findjob-jobs-preview", (findjobJobsPreview as { ok?: boolean }).ok === true && Array.isArray(findjobJobs?.jobs) && (findjobJobs.jobs.length ?? 0) > 0 && (findjobJobs._unidesk?.arrayLimits?.jobs?.returnedLength ?? 0) <= 5, findjobJobsPreview); addSelectedCheck(checks, options, "microservice:pipeline-status", (pipelineStatus as { ok?: boolean }).ok === true && (pipelineStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", pipelineStatus); addSelectedCheck(checks, options, "microservice:pipeline-health", (pipelineHealth as { ok?: boolean; body?: { ok?: boolean; service?: string } }).ok === true && (pipelineHealth as { body?: { ok?: boolean } }).body?.ok === true, pipelineHealth); addSelectedCheck(checks, options, "microservice:pipeline-snapshot", (pipelineSnapshot as { ok?: boolean }).ok === true && pipelineSnapshotBody?.ok === true && pipelineSnapshotBody.registry?.ok === true && Array.isArray(pipelineSnapshotBody.registry.components) && pipelineSnapshotBody.registry.components.length > 0 && Array.isArray(pipelineSnapshotBody.pipelines) && pipelineSnapshotBody.pipelines.length > 0 && Array.isArray(pipelineSnapshotBody.runs) && pipelineSnapshotBody.runs.length > 0 && (pipelineSnapshotBody._unidesk?.arrayLimits?.["registry.components"]?.returnedLength ?? 999) <= 8 && (pipelineSnapshotBody._unidesk?.arrayLimits?.runs?.returnedLength ?? 999) <= 3, pipelineSnapshotDetail); addSelectedCheck(checks, options, "microservice:pipeline-oa-event-flow", (pipelineOaEventFlow as { ok?: boolean }).ok === true && pipelineOaBody?.ok === true && pipelineOaBody.mode === "oa-event-flow-100" && Array.isArray(pipelineOaBody.forbiddenResiduals) && pipelineOaBody.forbiddenResiduals.length === 0 && pipelineOaBody.hasNeutralNodeFinishedEvidence === true && pipelineOaBody.hasNoAuditPolicyEvidence === true && pipelineOaBody.hasAuditPolicyEvidence === true && (pipelineOaBody.runs ?? []).some((run) => Number(run.noAuditPolicyCount || 0) > 0) && (pipelineOaBody.runs ?? []).some((run) => Number(run.monitorAuditNodeFinishedCount || 0) > 0 && Number(run.controlQueuedCount || 0) > 0 && Number(run.controlAppliedCount || 0) > 0) && (pipelineOaBody.runs ?? []).every((run) => Number(run.nodeFinishedWithPolicyCount || 0) === 0), { ok: (pipelineOaEventFlow as { ok?: boolean }).ok, mode: pipelineOaBody?.mode, forbiddenResiduals: pipelineOaBody?.forbiddenResiduals, hasNeutralNodeFinishedEvidence: pipelineOaBody?.hasNeutralNodeFinishedEvidence, hasNoAuditPolicyEvidence: pipelineOaBody?.hasNoAuditPolicyEvidence, hasAuditPolicyEvidence: pipelineOaBody?.hasAuditPolicyEvidence, runs: (pipelineOaBody?.runs ?? []).slice(0, 8), }); addSelectedCheck(checks, options, "microservice:met-nonlinear-status", (metNonlinearStatus as { ok?: boolean }).ok === true && (metNonlinearStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", metNonlinearStatus); addSelectedCheck(checks, options, "microservice:met-nonlinear-health", (metNonlinearHealth as { ok?: boolean }).ok === true && metNonlinearHealthBody?.ok === true, metNonlinearHealth); addSelectedCheck(checks, options, "microservice:met-nonlinear-queue", (metNonlinearQueue as { ok?: boolean }).ok === true && metNonlinearQueueBody?.ok === true && typeof metNonlinearQueueBody.queue?.counts === "object" && metNonlinearQueueBody.queue?.targetGpuName === "2080 Ti", metNonlinearQueue); addSelectedCheck(checks, options, "microservice:met-nonlinear-projects", (metNonlinearProjects as { ok?: boolean }).ok === true && metNonlinearProjectsBody?.ok === true && Array.isArray(metNonlinearProjectsBody.projects) && metNonlinearProjectsBody.projects.length > 0, metNonlinearProjects); addSelectedCheck(checks, options, "microservice:met-nonlinear-image", (metNonlinearImages as { ok?: boolean }).ok === true && metNonlinearImagesBody?.ok === true && metNonlinearImagesBody.mlImage?.present === true && metNonlinearImagesBody.mlImage?.image === "met-nonlinear-ml:tf26", metNonlinearImages); addSelectedCheck(checks, options, "microservice:claudeqq-status", (claudeqqStatus as { ok?: boolean }).ok === true && (claudeqqStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", claudeqqStatus); addSelectedCheck(checks, options, "microservice:claudeqq-health", (claudeqqHealth as { ok?: boolean }).ok === true && claudeqqHealthBody?.ok === true && claudeqqHealthBody.service === "claudeqq" && (claudeqqHealthBody.endpoints ?? []).includes("/api/push/text") && (claudeqqHealthBody.endpoints ?? []).includes("/api/napcat/login") && claudeqqHealthBody.napcat?.httpConnected === true && claudeqqHealthBody.napcat?.wsConnected === true && claudeqqHealthBody.napcat?.connected === true && claudeqqHealthBody.napcat?.loginState === "logged_in", claudeqqHealth); addSelectedCheck(checks, options, "microservice:claudeqq-napcat-login", (claudeqqNapcatLogin as { ok?: boolean }).ok === true && claudeqqNapcatLoginBody?.ok === true && claudeqqNapcatLoginBody.napcat?.containerized === true && claudeqqNapcatLoginBody.napcat?.httpConnected === true && claudeqqNapcatLoginBody.napcat?.wsConnected === true && claudeqqNapcatLoginBody.napcat?.connected === true && claudeqqNapcatLoginBody.login?.ready === true && claudeqqNapcatLoginBody.login?.state === "logged_in", claudeqqNapcatLogin); addSelectedCheck(checks, options, "microservice:claudeqq-events", (claudeqqEvents as { ok?: boolean }).ok === true && claudeqqEventsBody?.ok === true && Array.isArray(claudeqqEventsBody.events), claudeqqEvents); addSelectedCheck(checks, options, "microservice:claudeqq-subscriptions", (claudeqqSubscriptions as { ok?: boolean }).ok === true && claudeqqSubscriptionsBody?.ok === true && Array.isArray(claudeqqSubscriptionsBody.subscriptions), claudeqqSubscriptions); addSelectedCheck(checks, options, "microservice:todo-note-status", (todoNoteStatus as { ok?: boolean }).ok === true && (todoNoteStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, todoNoteStatus); addSelectedCheck(checks, options, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth); addSelectedCheck(checks, options, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances }); addSelectedCheck(checks, options, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete }); addSelectedCheck(checks, options, "microservice:oa-event-flow-status", (oaEventFlowStatus as { ok?: boolean }).ok === true && (oaEventFlowStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, oaEventFlowStatus); addSelectedCheck(checks, options, "microservice:oa-event-flow-health", (oaEventFlowHealth as { ok?: boolean }).ok === true && oaEventFlowHealthBody?.ok === true && oaEventFlowHealthBody.service === "oa-event-flow" && oaEventFlowHealthBody.databaseReady === true, oaEventFlowHealth); addSelectedCheck(checks, options, "microservice:oa-event-flow-diagnostics", (oaEventFlowDiagnostics as { ok?: boolean }).ok === true && oaEventFlowDiagnosticsBody?.ok === true && oaEventFlowDiagnosticsBody.service === "oa-event-flow" && oaEventFlowDiagnosticsBody.databaseReady === true && Number.isFinite(oaEventFlowDiagnosticsBody.eventCount) && Array.isArray(oaEventFlowDiagnosticsBody.eventTypes), oaEventFlowDiagnostics); addSelectedCheck(checks, options, "microservice:oa-event-flow-events", (oaEventFlowEvents as { ok?: boolean }).ok === true && oaEventFlowEventsBody?.ok === true && Array.isArray(oaEventFlowEventsBody.events), oaEventFlowEvents); addSelectedCheck(checks, options, "microservice:oa-event-flow-pipeline-bridge", (oaEventFlowPipelineEvents as { ok?: boolean }).ok === true && oaEventFlowPipelineEventsBody?.ok === true && Array.isArray(oaEventFlowPipelineEventsBody.events) && oaEventFlowPipelineEventsBody.events.some((event) => Array.isArray(event.tags) && event.tags.includes("service:pipeline") && event.tags.some((tag) => typeof tag === "string" && tag.startsWith("epoch:"))) && oaEventFlowDiagnosticsBody?.pipelineBridge?.enabled === true && String(oaEventFlowDiagnosticsBody.pipelineBridge.lastError || "").length === 0, { pipelineBridge: oaEventFlowDiagnosticsBody?.pipelineBridge, sampleEvents: oaEventFlowPipelineEventsBody?.events?.slice(0, 5), }); addSelectedCheck(checks, options, "microservice:oa-event-flow-stats", (oaEventFlowStats as { ok?: boolean }).ok === true && oaEventFlowStatsBody?.ok === true && Array.isArray(oaEventFlowStatsBody.stats), oaEventFlowStats); addSelectedCheck(checks, options, "microservice:code-queue-status", (codeQueueStatus as { ok?: boolean }).ok === true && codeQueueStatusProviderOk && codeQueueStatusProxyOk, codeQueueStatus); addSelectedCheck(checks, options, "microservice:code-queue-health", codeQueueHealthOk, codeQueueHealth); addSelectedCheck(checks, options, "microservice:code-queue-workdirs", (codeQueueWorkdirs as { ok?: boolean; status?: number } | null)?.ok === true && (codeQueueWorkdirs as { status?: number } | null)?.status === 200 && codeQueueWorkdirsBody?.ok === true && Array.isArray(codeQueueWorkdirsBody.workdirs) && codeQueueWorkdirsBody.workdirs.some((workdir) => workdir?.path === expectedCodeQueueWorkdir) && codeQueueWorkdirsBody.workdirs.every((workdir) => typeof workdir?.path === "string" && typeof workdir?.providerId === "string" && typeof workdir?.executionMode === "string"), codeQueueWorkdirs); addSelectedCheck(checks, options, "microservice:code-queue-tasks", (codeQueueTasks as { ok?: boolean }).ok === true && codeQueueTasksBody?.ok === true && Array.isArray(codeQueueTasksBody.tasks) && codeQueueTasksBody.queue?.defaultModel === "gpt-5.5" && codeQueueTasksBody.queue?.modelReasoningEfforts?.["gpt-5.5"] === "xhigh", codeQueueTasks); addSelectedCheck(checks, options, "microservice:decision-center-status", (decisionCenterStatus as { ok?: boolean }).ok === true && (decisionCenterStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", decisionCenterStatus); addSelectedCheck(checks, options, "microservice:decision-center-health", (decisionCenterHealth as { ok?: boolean }).ok === true && decisionCenterHealthBody?.ok === true && decisionCenterHealthBody.service === "decision-center" && decisionCenterHealthBody.storage === "postgres" && decisionCenterHealthBody.schemaReady === true, decisionCenterHealth); addSelectedCheck(checks, options, "microservice:decision-center-records", (decisionCenterRecords as { ok?: boolean }).ok === true && decisionCenterRecordsBody?.ok === true && Array.isArray(decisionCenterRecordsBody.records), decisionCenterRecords); addSelectedCheck(checks, options, "microservice:decision-center-record-crud", (decisionCenterRecordCreate as { ok?: boolean } | null)?.ok === true && (decisionCenterRecordUpdate as { ok?: boolean }).ok === true && (decisionCenterRecordRead as { ok?: boolean; body?: { record?: { id?: string; status?: string; level?: string; body?: string } } }).ok === true && (decisionCenterRecordRead as { body?: { record?: { id?: string; status?: string; level?: string; body?: string } } }).body?.record?.id === decisionCenterRecordId && (decisionCenterRecordRead as { body?: { record?: { status?: string; level?: string; body?: string } } }).body?.record?.status === "blocked" && (decisionCenterRecordRead as { body?: { record?: { status?: string; level?: string; body?: string } } }).body?.record?.level === "P1" && String((decisionCenterRecordRead as { body?: { record?: { body?: string } } }).body?.record?.body || "").includes("E2E edited") && (decisionCenterRecordDelete as { ok?: boolean }).ok === true, { marker: decisionCenterRecordMarker, recordId: decisionCenterRecordId, create: decisionCenterRecordCreate, update: decisionCenterRecordUpdate, read: decisionCenterRecordRead, delete: decisionCenterRecordDelete, }); addSelectedCheck(checks, options, "microservice:decision-center-diary-lifecycle", (decisionCenterDiaryCreate as { ok?: boolean } | null)?.ok === true && (decisionCenterDiaryEdit as { ok?: boolean } | null)?.ok === true && (decisionCenterDiaryRead as { ok?: boolean; body?: { entry?: { date?: string; title?: string; body?: string; sourceFile?: string; tags?: string[] } } } | null)?.ok === true && (decisionCenterDiaryRead as { body?: { entry?: { date?: string; title?: string; body?: string; sourceFile?: string; tags?: string[] } } } | null)?.body?.entry?.date === decisionCenterDiaryDate && String((decisionCenterDiaryRead as { body?: { entry?: { title?: string; body?: string } } } | null)?.body?.entry?.title || "").includes("Edited") && String((decisionCenterDiaryRead as { body?: { entry?: { title?: string; body?: string } } } | null)?.body?.entry?.body || "").includes(decisionCenterDiaryMarker) && String((decisionCenterDiaryRead as { body?: { entry?: { title?: string; body?: string } } } | null)?.body?.entry?.body || "").includes("Edited."), { date: decisionCenterDiaryDate, marker: decisionCenterDiaryMarker, create: decisionCenterDiaryCreate, edit: decisionCenterDiaryEdit, read: decisionCenterDiaryRead, }); const upgradeDispatch = dockerCoreJson("/api/dispatch", { method: "POST", body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } }, }); const upgradeTaskId = (upgradeDispatch as { body?: { taskId?: string } }).body?.taskId ?? ""; const upgradeTask = upgradeTaskId ? await waitForTaskStatus(upgradeTaskId, "succeeded") : { ok: false, error: "missing taskId", upgradeDispatch }; const taskResult = (upgradeTask as { task?: { result?: { plan?: { providerGatewayVersion?: string; targetProviderGatewayVersion?: string }; mode?: string } }; ok?: boolean }).task?.result; const upgradePlan = taskResult?.plan; addSelectedCheck(checks, options, "provider:upgrade-plan", (upgradeDispatch as { ok?: boolean }).ok === true && (upgradeTask as { ok?: boolean }).ok === true && taskResult?.mode === "plan" && upgradePlan !== undefined && upgradePlan.providerGatewayVersion === expectedGatewayVersion && upgradePlan.targetProviderGatewayVersion === expectedGatewayVersion, { expectedGatewayVersion, upgradeDispatch, upgradeTask }); addSelectedCheck(checks, options, "provider-ingress:public-health", (providerIngress as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (providerIngress as { body?: { ok?: boolean } }).body?.ok === true, providerIngress); } function databaseChecks(config: UniDeskConfig, checks: E2ECheck[], options: E2ERunOptions): string { const markerId = `e2e_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; const failedTaskId = `task_${markerId}_failed_diagnostic`; const markerSql = ` CREATE TABLE IF NOT EXISTS unidesk_e2e_markers ( id TEXT PRIMARY KEY, source TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO unidesk_e2e_markers (id, source) VALUES ('${markerId}', 'cli-e2e'); INSERT INTO unidesk_tasks (id, provider_id, command, status, payload, result, created_at, updated_at) VALUES ( '${failedTaskId}', '${config.providerGateway.id}', 'echo', 'failed', '{"source":"cli-e2e","case":"history-diagnostics"}'::jsonb, '{"error":"e2e forced failure for diagnostics","exitCode":23,"stderr":"simulated provider failure"}'::jsonb, now() - interval '83 seconds', now() ); SELECT 'marker=' || id FROM unidesk_e2e_markers WHERE id = '${markerId}'; SELECT 'marker_count=' || count(*) FROM unidesk_e2e_markers; SELECT 'failed_task=' || id FROM unidesk_tasks WHERE id = '${failedTaskId}' AND status = 'failed'; SELECT 'online_main_server=' || count(*) FROM unidesk_nodes WHERE provider_id = '${config.providerGateway.id}' AND status = 'online'; SELECT 'todo_note_instances=' || count(*) FROM todo_note_instances; WITH RECURSIVE todo_tree(instance_id, todo) AS ( SELECT id, jsonb_array_elements(todos) FROM todo_note_instances UNION ALL SELECT todo_tree.instance_id, child.value FROM todo_tree CROSS JOIN LATERAL jsonb_array_elements( CASE WHEN jsonb_typeof(todo_tree.todo->'children') = 'array' THEN todo_tree.todo->'children' ELSE '[]'::jsonb END ) AS child(value) ) SELECT 'todo_note_total_todos=' || count(*) FROM todo_tree; `; const marker = runPsql(config, markerSql); const todoNoteInstanceCount = Number(marker.stdout.match(/todo_note_instances=(\d+)/)?.[1] ?? NaN); const todoNoteTotalTodos = Number(marker.stdout.match(/todo_note_total_todos=(\d+)/)?.[1] ?? NaN); addSelectedCheck(checks, options, "database:named-volume-write", marker.ok && marker.stdout.includes(`marker=${markerId}`), marker); addSelectedCheck(checks, options, "database:provider-state", marker.ok && marker.stdout.includes("online_main_server=1"), marker); addSelectedCheck(checks, options, "database:todo-note-pg-storage", marker.ok && todoNoteInstanceCount >= 5 && todoNoteTotalTodos >= 100, { ...marker, todoNoteInstanceCount, todoNoteTotalTodos }); return markerId; } async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise<{ screenshotPath: string; bodyText: string; consoleErrors: string[] }> { const e2eDir = rootPath(".state", "e2e"); mkdirSync(e2eDir, { recursive: true }); const screenshotPath = join(e2eDir, `${new Date().toISOString().replace(/[-:.TZ]/g, "")}_frontend.png`); const consoleErrors: string[] = []; const wants = (name: string): boolean => wantsCheck(options, name); const wantsAny = (names: string[]): boolean => wantsAnyCheck(options, names); const needSidebar = wants("frontend:sidebar-collapse"); const needMobileRail = wants("frontend:mobile-nav-fixed-height"); const needMobileContent = wants("frontend:mobile-content-top-aligned"); const needPendingTask = wants("frontend:pending-task-drilldown") || needMobileContent; const needTaskHistory = wants("frontend:task-history-diagnostics"); const needOverviewBody = wantsAny([ "frontend:login-provider-visible", "frontend:overview-pgdata-visible", "frontend:no-naked-json-before-click", ]) || needPendingTask || needTaskHistory; const needDevAuthApiCookie = wants("frontend:dev-auth-api-cookie"); const needRawProviderJson = wantsAny([ "frontend:public-provider-info-visible", "frontend:raw-json-explicit-button", ]); const needNodeMonitor = wantsAny([ "frontend:system-monitor-visible", "frontend:process-resource-sorting", "frontend:upgrade-plan-dispatch", ]); const needPerformancePanel = wants("frontend:performance-panel-visible"); const needDocker = wants("frontend:docker-status-visible"); const needGatewayVersion = wantsAny([ "frontend:gateway-version-records-visible", "frontend:gateway-duration-subsecond-visible", "frontend:provider-operation-availability-visible", ]); const needMicroserviceCatalog = wants("frontend:microservice-catalog-visible"); const needTodoNote = wants("frontend:todo-note-integrated-visible"); const needFindJob = wants("frontend:findjob-integrated-visible"); const needOaEventFlow = wants("frontend:oa-event-flow-visible"); const needDecisionCenter = wantsAny([ "frontend:decision-center-visible", "frontend:decision-center-demand-management-visible", "frontend:decision-center-diary-visible", ]); const needCodeQueue = wantsAny([ "frontend:code-queue-integrated-visible", "frontend:code-queue-workdirs-loaded", "frontend:code-queue-enqueue-await-smoke", "frontend:code-queue-summary-mobile-wrap", "frontend:code-queue-initial-prompt-full-expand", "frontend:code-queue-trace-full-load", "frontend:code-queue-judge-wrap", "frontend:code-queue-error-red-markers", "frontend:code-queue-stats-visible", "frontend:code-queue-stats-degraded-visible", "frontend:code-queue-split-brain-live-not-failed", "frontend:code-queue-split-brain-stale-failed", "frontend:code-queue-retry-attempt-trace-current", "frontend:code-queue-step-missing-diagnostic", "frontend:code-queue-judge-feedback-attempt-order", ]); const needCodeQueueFullSurface = wantsAny([ "frontend:code-queue-integrated-visible", "frontend:code-queue-enqueue-await-smoke", "frontend:code-queue-summary-mobile-wrap", "frontend:code-queue-initial-prompt-full-expand", "frontend:code-queue-trace-full-load", "frontend:code-queue-judge-wrap", "frontend:code-queue-error-red-markers", "frontend:code-queue-stats-visible", "frontend:code-queue-stats-degraded-visible", ]); const needCodeQueueRetryTraceFixture = wantsAny([ "frontend:code-queue-retry-attempt-trace-current", "frontend:code-queue-step-missing-diagnostic", "frontend:code-queue-judge-feedback-attempt-order", ]); const needClaudeqq = wants("frontend:claudeqq-integrated-visible"); const needRouteDeepLink = wants("frontend:url-route-deeplink"); const needPipeline = wantsAny([ "frontend:pipeline-integrated-visible", "frontend:pipeline-react-flow-visible", "frontend:pipeline-sidebars-collapsible", "frontend:pipeline-gantt-defaults", "frontend:pipeline-gantt-frontend-y-accuracy", "frontend:pipeline-gantt-export", "frontend:pipeline-gantt-observation-live-running", "frontend:pipeline-step-timeline-visible", "frontend:pipeline-oa-event-flow-visible", "frontend:pipeline-minimax-quota-visible", ]); const needMetNonlinear = wantsAny([ "frontend:met-nonlinear-integrated-visible", "frontend:met-nonlinear-project-tree-detail", "frontend:met-nonlinear-queue-detail-speed", ]); const needLayoutOverflowDesktop = wants("frontend:layout-overflow-desktop"); const needLayoutOverflowMobile = wants("frontend:layout-overflow-mobile"); const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 920 } }); page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("pageerror", (error) => consoleErrors.push(error.message)); page.on("dialog", (dialog) => dialog.accept()); await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="login-screen"]', { timeout: 10000 }); const frontendAuthUsername = process.env.UNIDESK_E2E_AUTH_USERNAME || config.auth.username; const frontendAuthPassword = process.env.UNIDESK_E2E_AUTH_PASSWORD || config.auth.password; await page.fill('input[name="username"]', frontendAuthUsername); await page.fill('input[name="password"]', frontendAuthPassword); await page.click('button[type="submit"]'); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); await page.waitForFunction(() => document.querySelector('[data-testid="conn-text"]')?.textContent?.includes("核心在线"), undefined, { timeout: 15000 }); const landedUrl = page.url(); const publicOrigin = new URL(urls.frontendUrl).origin; const landed = new URL(landedUrl); const publicFrontendReached = landed.origin === publicOrigin && !["127.0.0.1", "localhost", "::1"].includes(landed.hostname); if (needOverviewBody || needRawProviderJson || needNodeMonitor || needDocker || needGatewayVersion) { await page.waitForSelector(`text=${config.providerGateway.id}`, { timeout: 10000 }); await page.waitForSelector(`text=${config.providerGateway.name}`, { timeout: 10000 }); } let railWidthBefore = 0; let railWidthCollapsed = 0; let mobileRailHeights: number[] = []; let mobileRailMax = 0; let mobileRailMin = 0; let mobileContentMetrics = { pageTop: 0, emptyTextOffset: 0 }; let bodyText = ""; let devAuthApiMetrics: any = { checked: false }; let rawBlocksBefore = 0; let nakedJsonText = false; let pendingTaskText = ""; let taskHistoryText = ""; let rawText = ""; let performanceText = ""; let monitorText = ""; let processTableText = ""; let processMemoryValues: number[] = []; let processDefaultMemoryDescending = false; let processMemorySortAria = ""; let processCpuValues: number[] = []; let processCpuDescending = false; let processCpuSortAria = ""; let upgradeControlText = ""; let dockerText = ""; let gatewayText = ""; let gatewayTextLower = ""; let gatewayHasSubsecondDuration = false; let gatewayHasRoundedZeroDuration = false; let sshAvailabilityTexts: string[] = []; let upgradeAvailabilityTexts: string[] = []; let microserviceCatalogText = ""; let todoNoteText = ""; let findjobText = ""; let oaEventFlowText = ""; let oaEventFlowMetrics: any = { pageVisible: false, eventTableVisible: false, statsVisible: false, tagFilterValue: "", rawButtonCount: 0 }; let decisionCenterText = ""; let decisionCenterE2eRecord: any = null; let decisionCenterDeleteResult: any = null; let decisionCenterMetrics: any = { pageVisible: false, tableVisible: false, rawButtonCount: 0, rawJsonBlocks: 0, chatInputCount: 0, bodyContainsRecord: false }; let decisionCenterDemandText = ""; let decisionCenterDemandMetrics: any = { workspaceVisible: false, editorVisible: false, lanes: {}, filterButtons: 0 }; let decisionCenterDiaryText = ""; let decisionCenterDiaryMetrics: any = { editorVisible: false, todayButtonVisible: false, dateValue: "", bodyValue: "", entryCards: 0, selectedHasMarker: false, duplicateDateCards: 0 }; let codeQueueText = ""; let codeQueueOutputText = ""; let codeQueueTaskCount = 0; let codeQueueOptions: string[] = []; let codeQueueSwitchMetrics: any = { optionCount: 0, switched: false }; let codeQueueSubmitQueueControl: any = { tagName: "", createButtonVisible: false, oldInputMissing: false, workdirSelectOptionTexts: [] }; let codeQueueTracePlacement: any = { firstChildIsTrace: false, noPageTopStatus: false, filterInsideTracePanel: false, traceStatusVisible: false, markAllReadVisible: false }; let codeQueueGlobalStatus: any = { activeMicroserviceVisible: false }; let codeQueueSidebarUpdateMetrics: any = { cardCount: 0, labels: [], hasRecentUpdateLabel: false }; let codeQueueHtmlGuard: any = { rootAttrMissing: false, sourceAttrMissing: false, sourceNoBasePrompt: false }; let codeQueueSummaryMobileMetrics: any = { checked: false, summaryCount: 0, ok: false }; let codeQueueLongPromptMetrics: any = { checked: false }; let codeQueuePromptDefaultEmpty = false; let codeQueueSubmitGuard: any = { batchRowVisible: false, disabledBeforeConfirm: false, enabledAfterConfirm: false, waitElementMissingBeforeSubmit: false }; let codeQueueEnqueueAwaitSmoke: any = { checked: false }; let codeQueueScrollbarMetrics: any = { transcriptThin: false, toolHorizontalHidden: true }; let codexInitialPromptFullMetrics: any = { candidateFound: false }; let codexTraceFullMetrics: any = { candidateFound: false }; let codexJudgeWrapMetrics: any = { checked: false }; let codeQueueErrorHighlightMetrics: any = { checked: false, candidateFound: false }; let codeQueueStatsVisibleMetrics: any = { checked: false }; let codeQueueStatsDegradedMetrics: any = { checked: false }; let codeQueueSplitBrainLiveMetrics: any = { checked: false }; let codeQueueSplitBrainStaleMetrics: any = { checked: false }; let codeQueueRetryTraceFixtureMetrics: any = { checked: false }; let claudeqqText = ""; let routeDeepLinkText = ""; let routeInitialPath = ""; let routeDockerPath = ""; let routeBackIntermediatePath = ""; let routeBackPath = ""; let routeOverviewPath = ""; let routeOverviewText = ""; let routeCodexPath = ""; let routeCodexShellMetrics: any = { appShell: false, standalone: false, railText: "", topbar: false, tabsText: "", codexPage: false }; let pipelineText = ""; let pipelineOaPanelText = ""; let pipelineMinimaxQuotaText = ""; let pipelinePriorityOrder: any = { desktop: [], mobile: [] }; let pipelineSidebarMetrics: any = { nodeDefaultOpen: "", nodeAfterClickOpen: "", nodeAfterCollapseOpen: "", nodeToggleEnabledAfterCollapse: false, ganttDefaultOpen: "", ganttLineCount: 0, ganttAfterClickOpen: "", ganttAfterCollapseOpen: "", ganttToggleEnabledAfterCollapse: false, mobileNodeCollapsed: "", mobileGanttCollapsed: "", }; let pipelineFlowNodeCount = 0; let pipelineFlowEdgeCount = 0; let pipelineSelectedId = ""; let pipelineGanttScaleLabel = ""; let pipelineGanttAutoHideIdleChecked = true; let pipelineGanttHeaderNodeOrder: string[] = []; let pipelineGanttFrontendYMetrics: any = { layoutSource: "", checked: 0, maxDelta: null, violations: [] }; let pipelineGanttExportInfo: any = { downloaded: false, suggestedFilename: "", savePath: "", bytes: 0 }; let pipelineSnapshotForFrontend: any = null; let pipelineObservationSetup: any = null; let pipelineObservationGanttMetrics: any = { candidate: null, observationArrowCount: 0, observationSourceMarkerCount: 0, observationArrowTargetInsetsPx: [], liveRunningBarCount: 0, liveRunningHeights: [], runningAnimationNames: [], arrowPairs: [], hasLiveSweep: false }; let pipelineStepTimelineText = ""; let pipelineSessionHeadText = ""; let firstPipelineStepSummaryText = ""; let pipelineTimelineMetrics = { clientWidth: 0, scrollWidth: 0, clientHeight: 0, scrollHeight: 0, hasHorizontalScroll: false, flowConnectorVisible: false, maxStepIdleGapMs: 0, idleGapCount: 0, oldPipelineStepStyleCount: 0, emptyAttemptDetailCount: 0, selectedNodeId: "", selectedProcedureRunId: "" }; let firstPipelineStepSummaryMetrics = { clientWidth: 0, scrollWidth: 0, clientHeight: 0, scrollHeight: 0, hasHorizontalScroll: false }; let firstPipelineStepExpandedText = ""; let metNonlinearInitialText = ""; let metProjectTreeText = ""; let metProjectDetailText = ""; let metCompletedText = ""; let metJobDetailText = ""; let layoutOverflowDesktop: OverflowProbe[] = []; let layoutOverflowMobile: OverflowProbe[] = []; if (needSidebar) { railWidthBefore = await page.locator(".rail").evaluate((element) => Math.round(element.getBoundingClientRect().width)); await page.getByTestId("rail-toggle").click(); await page.waitForTimeout(120); railWidthCollapsed = await page.locator(".rail").evaluate((element) => Math.round(element.getBoundingClientRect().width)); await page.getByTestId("rail-toggle").click(); await page.waitForTimeout(80); } if (wants("frontend:pipeline-gantt-observation-live-running")) { pipelineObservationSetup = await ensurePipelineLiveObservationCandidate(); } if (needMobileRail || needMobileContent) { await page.setViewportSize({ width: 390, height: 860 }); if (needMobileRail) { for (const moduleLabel of ["运行总览", "资源节点", "任务调度", "用户服务", "系统配置"]) { await page.getByRole("button", { name: new RegExp(moduleLabel) }).click(); await page.waitForTimeout(80); const height = await page.locator(".rail").evaluate((element) => Math.round(element.getBoundingClientRect().height)); mobileRailHeights.push(height); } mobileRailMax = Math.max(...mobileRailHeights); mobileRailMin = Math.min(...mobileRailHeights); } if (needMobileContent) { await page.getByRole("button", { name: /任务调度/ }).click(); await page.getByRole("button", { name: /待处理任务/ }).click(); await page.waitForSelector('[data-testid="pending-task-page"]', { timeout: 5000 }); mobileContentMetrics = await page.locator('[data-testid="pending-task-page"]').evaluate((element) => { const pageTop = element.getBoundingClientRect().top; const empty = element.querySelector(".empty-state"); const emptyBox = empty?.getBoundingClientRect(); const emptyStrong = empty?.querySelector("strong")?.getBoundingClientRect(); return { pageTop: Math.round(pageTop), emptyTextOffset: emptyBox && emptyStrong ? Math.round(emptyStrong.top - emptyBox.top) : 0, }; }); } await page.setViewportSize({ width: 1440, height: 920 }); } if (needOverviewBody || needRawProviderJson || needPendingTask || needTaskHistory || needPerformancePanel) { await page.getByRole("button", { name: /运行总览/ }).click(); await page.getByRole("button", { name: /态势总览/ }).click(); await page.waitForSelector('[data-testid="overview-page"]', { timeout: 5000 }); if (needOverviewBody) { bodyText = await page.locator("body").innerText({ timeout: 5000 }); rawBlocksBefore = await page.locator("pre.raw-json").count(); nakedJsonText = bodyText.includes('{"') || bodyText.includes('"providerId"') || bodyText.includes('"labels"'); } if (needPendingTask) { await page.getByTestId("pending-task-card").click(); await page.waitForSelector('[data-testid="pending-task-page"]', { timeout: 5000 }); pendingTaskText = await page.locator('[data-testid="pending-task-page"]').innerText({ timeout: 5000 }); } if (needTaskHistory) { await page.getByRole("button", { name: /任务历史/ }).click(); await page.waitForSelector('[data-testid="task-history-page"]', { timeout: 5000 }); taskHistoryText = await page.locator('[data-testid="task-history-page"]').innerText({ timeout: 5000 }); } if (needRawProviderJson) { await page.getByRole("button", { name: /运行总览/ }).click(); await page.getByRole("button", { name: /态势总览/ }).click(); await page.getByTestId(`raw-node-${config.providerGateway.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`).click(); await page.waitForSelector('[data-testid="raw-json"]', { timeout: 5000 }); rawText = await page.locator('[data-testid="raw-json"]').innerText({ timeout: 5000 }); await page.getByRole("button", { name: "关闭" }).click(); } if (needPerformancePanel) { await page.getByRole("button", { name: /性能面板/ }).click(); await page.waitForSelector('[data-testid="performance-page"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="performance-memory-chart"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; return text.includes("性能面板") && text.includes("组件汇总") && text.includes("最近失败请求") && text.includes("内部操作汇总") && text.includes("最近慢操作") && text.includes("Bwebui"); }, undefined, { timeout: 15000 }); performanceText = await page.locator('[data-testid="performance-page"]').innerText({ timeout: 5000 }); } } if (needNodeMonitor || needDocker || needGatewayVersion) { await page.getByRole("button", { name: /资源节点/ }).click(); if (needNodeMonitor) { await page.getByRole("button", { name: /资源监控/ }).click(); await page.waitForSelector('[data-testid="node-monitor-page"]', { timeout: 10000 }); await page.locator('[data-testid="node-monitor-page"]').getByRole("button", { name: new RegExp(config.providerGateway.id) }).click(); await page.waitForSelector('[data-testid="metric-chart-cpu"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="metric-chart-memory"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="metric-chart-disk"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="process-resource-table"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText.toLowerCase(); return text.includes("任务管理器视图") && text.includes("cpu") && text.includes("memory") && text.includes("disk") && text.includes("不含缓存") && text.includes("进程资源占用"); }, undefined, { timeout: 10000 }); monitorText = await page.locator('[data-testid="node-monitor-page"]').innerText({ timeout: 5000 }); processTableText = await page.locator('[data-testid="process-resource-table"]').innerText({ timeout: 5000 }); processMemoryValues = await page.locator('[data-testid="process-resource-table"] tbody tr').evaluateAll((rows) => rows.map((row) => Number((row as HTMLElement).dataset.memoryBytes || "0"))); processDefaultMemoryDescending = processMemoryValues.length > 0 && processMemoryValues.every((value, index, rows) => index === 0 || rows[index - 1] >= value); processMemorySortAria = await page.getByTestId("process-sort-memory").evaluate((element) => element.closest("th")?.getAttribute("aria-sort") || ""); await page.getByTestId("process-sort-cpu").click(); await page.waitForFunction(() => document.querySelector('[data-testid="process-sort-cpu"]')?.closest("th")?.getAttribute("aria-sort") === "descending", undefined, { timeout: 5000 }); processCpuValues = await page.locator('[data-testid="process-resource-table"] tbody tr').evaluateAll((rows) => rows.map((row) => Number((row as HTMLElement).dataset.cpuPercent || "0"))); processCpuDescending = processCpuValues.length > 0 && processCpuValues.every((value, index, rows) => index === 0 || rows[index - 1] >= value); processCpuSortAria = await page.getByTestId("process-sort-cpu").evaluate((element) => element.closest("th")?.getAttribute("aria-sort") || ""); await page.getByTestId("upgrade-plan-button").click(); await page.waitForFunction(() => document.body.innerText.includes("预检升级 已下发"), undefined, { timeout: 10000 }); upgradeControlText = await page.locator('[data-testid="provider-upgrade-control"]').innerText({ timeout: 5000 }); } if (needDocker) { await page.getByRole("button", { name: /Docker 状态/ }).click(); await page.waitForSelector('[data-testid="docker-status-page"]', { timeout: 10000 }); await page.locator('[data-testid="docker-status-page"]').getByRole("button", { name: new RegExp(config.providerGateway.id) }).click(); await page.waitForSelector('[data-testid="docker-container-table"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="database-volume-card"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText.toLowerCase(); return text.includes("docker desktop 视图") && text.includes("containers") && text.includes("unidesk_pgdata_10gb"); }, undefined, { timeout: 10000 }); dockerText = await page.locator('[data-testid="docker-status-page"]').innerText({ timeout: 5000 }); } if (needGatewayVersion) { await page.getByRole("button", { name: /网关版本/ }).click(); await page.waitForSelector('[data-testid="gateway-version-page"]', { timeout: 10000 }); await page.waitForSelector(`[data-testid="gateway-version-${safeTestId(config.providerGateway.id)}"]`, { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText.toLowerCase(); return text.includes("provider gateway 版本") && text.includes("远程更新记录") && text.includes("provider.upgrade") && text.includes("ssh 透传") && text.includes("远程更新"); }, undefined, { timeout: 10000 }); gatewayText = await page.locator('[data-testid="gateway-version-page"]').innerText({ timeout: 5000 }); gatewayTextLower = gatewayText.toLowerCase(); gatewayHasSubsecondDuration = /\b\d+\.\d+s\b|<0\.01s/.test(gatewayText); gatewayHasRoundedZeroDuration = /(^|\s)0s($|\s)/.test(gatewayText); sshAvailabilityTexts = await page.locator('[data-testid="gateway-version-page"] [data-testid^="ssh-availability-"]').evaluateAll((elements) => elements.map((element) => (element as HTMLElement).innerText)); upgradeAvailabilityTexts = await page.locator('[data-testid="gateway-version-page"] [data-testid^="upgrade-availability-"]').evaluateAll((elements) => elements.map((element) => (element as HTMLElement).innerText)); } } if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needDecisionCenter || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { await page.getByRole("button", { name: /用户服务/ }).click(); if (needMicroserviceCatalog || needTodoNote || needFindJob || needOaEventFlow || needDecisionCenter || needCodeQueue || needClaudeqq || needRouteDeepLink || needPipeline || needMetNonlinear) { await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 }); } if (needMicroserviceCatalog) { await page.waitForSelector('[data-testid="microservice-row-findjob"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-pipeline"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-met-nonlinear"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-claudeqq"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-todo-note"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-oa-event-flow"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-code-queue"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="microservice-row-decision-center"]', { timeout: 10000 }); microserviceCatalogText = await page.locator('[data-testid="microservice-catalog-page"]').innerText({ timeout: 5000 }); } if (needTodoNote) { await page.getByRole("button", { name: /Todo Note/ }).click(); await page.waitForSelector('[data-testid="todo-note-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); return lower.includes("todo note 工作台") && text.includes("CONSTAR") && text.includes("大论文") && text.includes("找工作") && text.includes("小论文") && text.includes("事务") && text.includes("仅 UniDesk frontend 代理访问"); }, undefined, { timeout: 30000 }); const uiTodoListName = `UI E2E ${Date.now()}`; await page.getByLabel("新清单名称").fill(uiTodoListName); await page.getByRole("button", { name: "创建" }).click(); const uiTodoRow = page.locator(".todo-instance-row", { hasText: uiTodoListName }); await uiTodoRow.waitFor({ state: "visible", timeout: 10000 }); await uiTodoRow.click(); await page.waitForFunction((name) => { const active = document.querySelector('[data-testid="todo-note-page"] .todo-instance-row.active') as HTMLElement | null; return active?.innerText.includes(String(name)); }, uiTodoListName, { timeout: 10000 }); await page.waitForSelector('[data-testid="todo-note-tree"]', { timeout: 10000 }); await page.getByLabel("新增根任务").fill("UI E2E smoke task"); await page.getByRole("button", { name: "新增" }).click(); await page.waitForSelector("text=UI E2E smoke task", { timeout: 10000 }); todoNoteText = await page.locator('[data-testid="todo-note-page"]').innerText({ timeout: 5000 }); await uiTodoRow.click(); await page.waitForFunction((name) => { const active = document.querySelector('[data-testid="todo-note-page"] .todo-instance-row.active') as HTMLElement | null; return active?.innerText.includes(String(name)); }, uiTodoListName, { timeout: 10000 }); await page.getByRole("button", { name: "删除清单" }).click(); await page.waitForFunction((name) => !document.body.innerText.includes(String(name)), uiTodoListName, { timeout: 10000 }); } if (needFindJob) { await page.getByRole("button", { name: /FindJob/ }).click(); await page.waitForSelector('[data-testid="findjob-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText.toLowerCase(); const originalText = document.body.innerText; return text.includes("findjob 工作台".toLowerCase()) && text.includes("岗位总量") && text.includes("d601") && text.includes("近期岗位") && /岗位总量\s+\d+/.test(originalText) && /health\s+ok/i.test(originalText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(originalText); }, undefined, { timeout: 30000 }); findjobText = await page.locator('[data-testid="findjob-page"]').innerText({ timeout: 5000 }); } if (needOaEventFlow) { await page.getByRole("button", { name: /OA Event Flow/ }).click(); await page.waitForSelector('[data-testid="oa-event-flow-page"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="oa-event-flow-event-table"]', { timeout: 30000 }); await page.waitForSelector('[data-testid="oa-event-flow-stats"]', { timeout: 30000 }); oaEventFlowText = await page.locator('[data-testid="oa-event-flow-page"]').innerText({ timeout: 5000 }); oaEventFlowMetrics = await page.evaluate(() => { const pageRoot = document.querySelector('[data-testid="oa-event-flow-page"]') as HTMLElement | null; const eventTable = document.querySelector('[data-testid="oa-event-flow-event-table"]') as HTMLElement | null; const stats = document.querySelector('[data-testid="oa-event-flow-stats"]') as HTMLElement | null; const filter = document.querySelector('[data-testid="oa-event-flow-tag-filter"]') as HTMLInputElement | null; return { pageVisible: Boolean(pageRoot), eventTableVisible: Boolean(eventTable), statsVisible: Boolean(stats), tagFilterValue: filter?.value || "", rawButtonCount: pageRoot?.querySelectorAll('[data-testid^="raw-oa-"]').length ?? 0, textPreview: pageRoot?.innerText.slice(0, 800) || "", }; }); } if (needDecisionCenter) { const decisionCenterMarker = Date.now(); const decisionCenterE2eTitle = `E2E Decision Center ${decisionCenterMarker}`; const decisionCenterExternalGoalTitle = `E2E External Goal ${decisionCenterMarker}`; const decisionCenterDiaryDate = "2099-12-30"; const decisionCenterDiaryMarker = `E2E diary marker ${decisionCenterMarker}`; decisionCenterE2eRecord = await page.evaluate(async ({ title, externalGoalTitle }) => { const response = await fetch("/api/microservices/decision-center/proxy/api/records", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "meeting", level: "G1", status: "active", title, body: "E2E seeded meeting record for Decision Center frontend validation.", tags: ["e2e", "decision-center"], evidenceLinks: ["https://example.com/unidesk/decision-center-e2e"], }), }); const meeting = await response.json().catch(() => null); const goalResponse = await fetch("/api/microservices/decision-center/proxy/api/records", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "goal", level: "G0", status: "active", title: externalGoalTitle, body: "E2E seeded external goal for demand-management visibility.", tags: ["e2e", "external-goal"], }), }); const goal = await goalResponse.json().catch(() => null); return { ok: response.ok && goalResponse.ok, status: response.status, body: meeting, goalStatus: goalResponse.status, goal, }; }, { title: decisionCenterE2eTitle, externalGoalTitle: decisionCenterExternalGoalTitle }); const decisionCenterE2eGoalId = String(decisionCenterE2eRecord?.goal?.record?.id || ""); await page.getByRole("button", { name: /Decision Center/ }).click(); await page.waitForSelector('[data-testid="decision-center-page"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="decision-center-filters"]', { timeout: 30000 }); await page.waitForSelector('[data-testid="decision-center-record-table"]', { timeout: 30000 }); await page.waitForFunction((title) => { const text = document.body.innerText; return text.includes("Decision Center") && text.includes("G0/G1 目标") && text.includes("P0/P1 Blocker") && text.includes("停放事项") && text.includes("最近会议/决议") && text.includes("查看原始JSON") && text.includes(String(title)); }, decisionCenterE2eTitle, { timeout: 30000 }); await page.waitForSelector('[data-testid="requirement-workspace"]', { timeout: 10000 }); decisionCenterText = await page.locator('[data-testid="decision-center-page"]').innerText({ timeout: 5000 }); decisionCenterMetrics = await page.evaluate(() => { const root = document.querySelector('[data-testid="decision-center-page"]') as HTMLElement | null; const table = document.querySelector('[data-testid="decision-center-record-table"]') as HTMLElement | null; return { pageVisible: Boolean(root), tableVisible: Boolean(table), rawButtonCount: root?.querySelectorAll('[data-testid^="raw-decision-center"], .ghost-btn').length ?? 0, rawJsonBlocks: root?.querySelectorAll("pre.raw-json, [data-testid='raw-json']").length ?? 0, chatInputCount: root?.querySelectorAll("textarea, [contenteditable='true']").length ?? 0, recordCardCount: root?.querySelectorAll('[data-testid^="decision-record-"]').length ?? 0, tableRows: table?.querySelectorAll("tbody tr").length ?? 0, textPreview: root?.innerText.slice(0, 1000) || "", }; }); decisionCenterDemandText = await page.locator('[data-testid="decision-center-page"]').innerText({ timeout: 5000 }); decisionCenterDemandMetrics = await page.evaluate(() => { const root = document.querySelector('[data-testid="decision-center-page"]') as HTMLElement | null; const workspace = root?.querySelector('[data-testid="requirement-workspace"]') as HTMLElement | null; const laneIds = ["external-goal", "internal-goal", "blocker", "parked", "authority"]; const lanes = Object.fromEntries(laneIds.map((id) => [id, Boolean(root?.querySelector(`[data-testid="requirement-lane-${id}"]`))])); return { workspaceVisible: Boolean(workspace), editorVisible: Boolean(root?.querySelector('[data-testid="record-editor"]')), filtersVisible: Boolean(root?.querySelector('[data-testid="decision-center-filters"]')), filterButtons: root?.querySelectorAll('[data-testid^="requirement-filter-"]').length ?? 0, lanes, tableRows: root?.querySelectorAll('[data-testid="decision-center-record-table"] tbody tr').length ?? 0, textPreview: root?.innerText.slice(0, 1000) || "", }; }); await page.getByTestId("decision-tab-diary").click(); await page.waitForSelector('[data-testid="diary-editor"]', { timeout: 10000 }); await page.getByTestId("today-diary").click(); await page.waitForFunction(() => { const input = document.querySelector('[data-testid="diary-date-input"]') as HTMLInputElement | null; return /^\d{4}-\d{2}-\d{2}$/u.test(input?.value || ""); }, undefined, { timeout: 10000 }); await page.locator('[data-testid="diary-date-input"]').fill(decisionCenterDiaryDate); await page.locator('[data-testid="diary-body-editor"]').fill(`# ${decisionCenterDiaryDate}\n${decisionCenterDiaryMarker}\n`); await page.getByTestId("save-diary-button").click(); await page.waitForFunction((marker) => document.body.innerText.includes(String(marker)), decisionCenterDiaryMarker, { timeout: 30000 }); decisionCenterDiaryText = await page.locator('[data-testid="decision-center-page"]').innerText({ timeout: 5000 }); decisionCenterDiaryMetrics = await page.evaluate(({ date, marker }) => { const root = document.querySelector('[data-testid="decision-center-page"]') as HTMLElement | null; const dateInput = root?.querySelector('[data-testid="diary-date-input"]') as HTMLInputElement | null; const bodyInput = root?.querySelector('[data-testid="diary-body-editor"]') as HTMLTextAreaElement | null; const dateCards = Array.from(root?.querySelectorAll(`[data-testid="diary-entry-${date}"]`) ?? []); return { editorVisible: Boolean(root?.querySelector('[data-testid="diary-editor"]')), todayButtonVisible: Boolean(root?.querySelector('[data-testid="today-diary"]')) && Boolean(root?.querySelector('[data-testid="today-diary-button"]')), dateValue: dateInput?.value || "", bodyValue: bodyInput?.value || "", entryCards: root?.querySelectorAll('[data-testid^="diary-entry-"]').length ?? 0, duplicateDateCards: dateCards.length, selectedHasMarker: Boolean(root?.innerText.includes(String(marker))), textPreview: root?.innerText.slice(0, 1000) || "", }; }, { date: decisionCenterDiaryDate, marker: decisionCenterDiaryMarker }); const decisionCenterRecordId = String(decisionCenterE2eRecord?.body?.record?.id || ""); if (decisionCenterRecordId) { decisionCenterDeleteResult = await page.evaluate(async (id) => { const response = await fetch(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(String(id))}`, { method: "DELETE", credentials: "same-origin", }); return { ok: response.ok, status: response.status, body: await response.json().catch(() => null) }; }, decisionCenterRecordId); } if (decisionCenterE2eGoalId) { await page.evaluate(async (id) => { await fetch(`/api/microservices/decision-center/proxy/api/records/${encodeURIComponent(String(id))}`, { method: "DELETE", credentials: "same-origin", }).catch(() => null); }, decisionCenterE2eGoalId); } } if (needCodeQueue) { await page.getByLabel("用户服务 子功能").getByRole("button", { name: "Code Queue" }).click(); await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); return lower.includes("code queue") && text.includes("gpt-5.4-mini") && text.includes("gpt-5.4") && text.includes("gpt-5.5") && text.includes("仅 UniDesk frontend 代理访问") && text.includes("提交任务") && text.includes("入队份数") && text.includes("追加 prompt") && text.includes("打断") && lower.includes("attempts"); }, undefined, { timeout: 30000 }); await page.waitForSelector('[data-testid="code-queue-filter-select"]', { timeout: 10000 }); codeQueueTracePlacement = await page.evaluate(() => ({ firstChildIsTrace: Boolean(document.querySelector('[data-testid="code-queue-page"] > .codex-session-stage:first-child .codex-output-panel')), noPageTopStatus: document.querySelector('[data-testid="code-queue-page"] > [data-testid="codex-top-status"]') === null, filterInsideTracePanel: Boolean(document.querySelector('.codex-output-panel [data-testid="code-queue-filter-select"]')), taskSearchVisible: Boolean(document.querySelector('[data-testid="codex-task-search-input"]')), traceStatusVisible: /排队\s*\d+.*运行\s*\d+.*结束未读\s*\d+/s.test(document.querySelector('[data-testid="codex-trace-status-summary"]')?.textContent || ""), markAllReadVisible: Boolean(document.querySelector('.codex-output-panel [data-testid="codex-mark-all-read-button"]')), })); codeQueueGlobalStatus = await page.evaluate(() => { const text = document.querySelector('[data-testid="active-microservice-status"]')?.textContent || ""; return { activeMicroserviceVisible: /Code Queue.*在线|code queue.*在线/i.test(text), text }; }); await page.waitForSelector('[data-testid="code-queue-id-select"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="codex-create-queue-button"]', { timeout: 10000 }); codeQueueSubmitQueueControl = await page.evaluate(() => { const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const button = document.querySelector('[data-testid="codex-create-queue-button"]') as HTMLButtonElement | null; const mergeButton = document.querySelector('[data-testid="codex-merge-queue-button"]') as HTMLButtonElement | null; const prompt = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; const provider = document.querySelector('[data-testid="codex-provider-select"]') as HTMLSelectElement | null; const cwd = document.querySelector('[data-testid="codex-cwd-input"]') as HTMLInputElement | null; const cwdSelect = document.querySelector('[data-testid="codex-cwd-select"]') as HTMLSelectElement | null; const maxAttempts = document.querySelector('[data-testid="codex-max-attempts-input"]') as HTMLInputElement | null; const moveSelect = document.querySelector('[data-testid="codex-task-queue-move-select"]') as HTMLSelectElement | null; const moveButton = document.querySelector('[data-testid="codex-task-queue-move-button"]') as HTMLButtonElement | null; return { tagName: select?.tagName.toLowerCase() || "", optionCount: select?.options.length ?? 0, createButtonVisible: Boolean(button && button.offsetParent !== null), mergeButtonVisible: Boolean(mergeButton && mergeButton.offsetParent !== null), mergeButtonDisabled: Boolean(mergeButton?.disabled), mergeSourceInlineMissing: document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]') === null, mergeDialogMissingBeforeClick: document.querySelector('[data-testid="codex-merge-queue-dialog"]') === null, oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null, promptDefaultEmpty: (prompt?.value || "") === "", providerValue: provider?.value || "", providerOptions: Array.from(provider?.options || []).map((option) => ({ value: option.value, text: option.textContent || "" })), cwdValue: cwd?.value || "", workdirSelectValue: cwdSelect?.value || "", workdirSelectOptionTexts: Array.from(cwdSelect?.options || []).map((option) => option.textContent || ""), maxAttemptsMax: maxAttempts?.max || "", maxAttemptsValue: maxAttempts?.value || "", moveQueueVisible: Boolean(moveSelect && moveSelect.offsetParent !== null && moveButton && moveButton.offsetParent !== null), }; }); codeQueuePromptDefaultEmpty = Boolean(codeQueueSubmitQueueControl.promptDefaultEmpty); await page.locator('[data-testid="code-queue-task-form"] textarea').fill("e2e batch guard one\n---\ne2e batch guard two"); await page.waitForSelector('[data-testid="codex-batch-confirm-row"]', { timeout: 5000 }); codeQueueSubmitGuard = await page.evaluate(() => { const row = document.querySelector('[data-testid="codex-batch-confirm-row"]') as HTMLElement | null; const checkbox = document.querySelector('[data-testid="codex-batch-confirm-checkbox"]') as HTMLInputElement | null; const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; const wait = document.querySelector('[data-testid="codex-submit-wait"]') as HTMLElement | null; return { batchRowVisible: Boolean(row && row.offsetParent !== null), checkboxVisible: Boolean(checkbox && checkbox.offsetParent !== null), disabledBeforeConfirm: Boolean(button?.disabled), buttonTextBeforeConfirm: button?.textContent || "", waitElementMissingBeforeSubmit: wait === null, }; }); await page.locator('[data-testid="codex-batch-confirm-checkbox"]').check(); await page.waitForFunction(() => { const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return button !== null && !button.disabled; }, undefined, { timeout: 5000 }); codeQueueSubmitGuard = { ...codeQueueSubmitGuard, ...(await page.evaluate(() => { const button = document.querySelector('[data-testid="codex-enqueue-button"]') as HTMLButtonElement | null; return { enabledAfterConfirm: Boolean(button && !button.disabled), buttonTextAfterConfirm: button?.textContent || "", }; })), }; await page.locator('[data-testid="codex-clear-input-button"]').click(); if (wants("frontend:code-queue-enqueue-await-smoke")) { codeQueueEnqueueAwaitSmoke = await runCodeQueueEnqueueAwaitSmoke(page); } if (wants("frontend:code-queue-long-prompt-observation")) { codeQueueLongPromptMetrics = await runCodeQueueLongPromptObservation(page); } codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({ optionCount: options.length, queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"), switched: false, })); if (needCodeQueueFullSurface) { await page.waitForSelector('[data-testid="codex-output"]', { timeout: 10000 }); await page.waitForFunction(() => { const taskCount = document.querySelectorAll('[data-testid^="codex-task-codex_"]').length; const output = document.querySelector('[data-testid="codex-output"]')?.textContent || ""; return taskCount === 0 || output.includes("Submitted prompt"); }, undefined, { timeout: 15000 }); codeQueueScrollbarMetrics = await page.evaluate(() => { const transcript = document.querySelector('.codex-transcript') as HTMLElement | null; const toolBlock = document.querySelector('.codex-transcript-item.ran .codex-transcript-command, .codex-transcript-item.ran .codex-transcript-body, .codex-transcript-item.explored .codex-transcript-command, .codex-transcript-item.explored .codex-transcript-body, .codex-transcript-item.edited .codex-transcript-command, .codex-transcript-item.edited .codex-transcript-body') as HTMLElement | null; const transcriptStyle = transcript ? getComputedStyle(transcript) : null; const toolStyle = toolBlock ? getComputedStyle(toolBlock) : null; return { transcriptThin: transcriptStyle?.scrollbarWidth === "thin", transcriptScrollbarWidth: transcriptStyle?.scrollbarWidth || "", toolFound: toolBlock !== null, toolHorizontalHidden: toolBlock === null || toolStyle?.scrollbarWidth === "none", toolScrollbarWidth: toolStyle?.scrollbarWidth || "", toolOverflowX: toolStyle?.overflowX || "", }; }); } if (wants("frontend:code-queue-judge-wrap")) { codexJudgeWrapMetrics = await page.evaluate(() => { const measure = (element: HTMLElement | null): any => { if (element === null) return { found: false }; const style = getComputedStyle(element); const rect = element.getBoundingClientRect(); const horizontalOverflowPx = Math.max(0, element.scrollWidth - element.clientWidth); return { found: true, tag: element.tagName.toLowerCase(), className: String(element.className || ""), clientWidth: Math.round(element.clientWidth), scrollWidth: Math.round(element.scrollWidth), offsetWidth: Math.round(element.offsetWidth), rectWidth: Math.round(rect.width), horizontalOverflowPx: Math.round(horizontalOverflowPx), noHorizontalOverflow: horizontalOverflowPx <= 1, whiteSpace: style.whiteSpace, overflowWrap: style.overflowWrap, wordBreak: style.wordBreak, overflowX: style.overflowX, }; }; const longReason = `judge-regression-${"x".repeat(420)}`; const longContinuePrompt = `continue-${"y".repeat(420)}\nnext-line-${"z".repeat(420)}`; const probe = document.createElement("section"); probe.className = "codex-progressive-card codex-progressive-judge"; probe.setAttribute("data-testid", "codex-judge-wrap-probe"); probe.style.cssText = "position: fixed; left: 16px; top: 16px; width: 320px; max-width: 320px; opacity: 0; pointer-events: none; z-index: -1;"; const card = document.createElement("div"); card.className = "codex-judge-card"; card.setAttribute("data-testid", "codex-judge-wrap-probe-card"); const badge = document.createElement("span"); badge.className = "status-badge retry"; badge.textContent = "retry"; const confidence = document.createElement("strong"); confidence.textContent = "92% confidence"; const reason = document.createElement("p"); reason.setAttribute("data-testid", "codex-judge-wrap-probe-reason"); reason.textContent = longReason; const continuePrompt = document.createElement("pre"); continuePrompt.setAttribute("data-testid", "codex-judge-wrap-probe-continue"); continuePrompt.textContent = longContinuePrompt; card.append(badge, confidence, reason, continuePrompt); probe.append(card); document.body.append(probe); const probeMetrics = { card: measure(card), reason: measure(reason), continuePrompt: measure(continuePrompt), }; probe.remove(); const actualCards = Array.from(document.querySelectorAll('[data-testid$="-judge-card"], [data-testid="codex-task-judge-card"], .codex-progressive-judge .codex-judge-card')) .filter((element): element is HTMLElement => element instanceof HTMLElement) .slice(0, 8) .map((element) => measure(element)); const noActualOverflow = actualCards.every((item: any) => item.noHorizontalOverflow === true); const reasonWraps = probeMetrics.reason.overflowWrap === "anywhere" || probeMetrics.reason.wordBreak === "break-word"; const continueWraps = probeMetrics.continuePrompt.whiteSpace === "pre-wrap" && (probeMetrics.continuePrompt.overflowWrap === "anywhere" || probeMetrics.continuePrompt.wordBreak === "break-word"); return { checked: true, tokenLength: longReason.length, probeWidth: 320, probe: probeMetrics, actualCardCount: actualCards.length, actualCards, reasonWraps, continueWraps, noActualOverflow, ok: probeMetrics.card.noHorizontalOverflow === true && probeMetrics.reason.noHorizontalOverflow === true && probeMetrics.continuePrompt.noHorizontalOverflow === true && reasonWraps && continueWraps && noActualOverflow, }; }); } codeQueueSidebarUpdateMetrics = await page.evaluate(() => { const cards = Array.from(document.querySelectorAll('[data-testid="codex-session-sidebar"] .codex-task-card')) as HTMLElement[]; const labels = cards .map((card) => card.querySelector('[data-testid^="codex-task-recent-update-"]')?.textContent || "") .filter(Boolean); return { cardCount: cards.length, labels, hasRecentUpdateLabel: cards.length === 0 || labels.some((text) => /^最近更新:\s*(刚刚|--|\d+秒前|\d+分钟\d+秒前|\d+小时|\d+天)/u.test(text)), }; }); if (wantsAny(["frontend:code-queue-integrated-visible", "frontend:code-queue-summary-mobile-wrap"])) { await page.setViewportSize({ width: 390, height: 860 }); await page.waitForTimeout(120); codeQueueSummaryMobileMetrics = await page.evaluate(() => { const viewportWidth = document.documentElement.clientWidth; const summaries = Array.from(document.querySelectorAll(".codex-execution-summary")) as HTMLElement[]; const items = summaries.slice(0, 8).map((summary, index) => { const header = summary.querySelector(".codex-progressive-card-head") as HTMLElement | null; const meta = header?.querySelector("code") as HTMLElement | null; const headerRect = header?.getBoundingClientRect(); const metaRect = meta?.getBoundingClientRect(); const rootOverflowPx = Math.max(0, summary.scrollWidth - summary.clientWidth); const headerOverflowPx = header ? Math.max(0, header.scrollWidth - header.clientWidth) : 0; const metaOverflowPx = meta ? Math.max(0, meta.scrollWidth - meta.clientWidth) : 0; return { index, text: (header?.textContent || "").replace(/\s+/g, " ").trim().slice(0, 180), viewportWidth, rootClientWidth: Math.round(summary.clientWidth), rootScrollWidth: Math.round(summary.scrollWidth), headerClientWidth: header ? Math.round(header.clientWidth) : 0, headerScrollWidth: header ? Math.round(header.scrollWidth) : 0, metaClientWidth: meta ? Math.round(meta.clientWidth) : 0, metaScrollWidth: meta ? Math.round(meta.scrollWidth) : 0, rootOverflowPx: Math.round(rootOverflowPx), headerOverflowPx: Math.round(headerOverflowPx), metaOverflowPx: Math.round(metaOverflowPx), headerRight: headerRect ? Math.round(headerRect.right) : 0, metaRight: metaRect ? Math.round(metaRect.right) : 0, metaWhiteSpace: meta ? getComputedStyle(meta).whiteSpace : "", metaOverflowWrap: meta ? getComputedStyle(meta).overflowWrap : "", }; }); return { checked: true, viewportWidth, summaryCount: summaries.length, items, ok: summaries.length === 0 || items.every((item) => item.rootOverflowPx <= 1 && item.headerOverflowPx <= 1 && item.metaOverflowPx <= 1 && item.headerRight <= viewportWidth + 2 && item.metaRight <= viewportWidth + 2 && item.metaWhiteSpace !== "nowrap" && (item.metaOverflowWrap === "anywhere" || item.metaOverflowWrap === "break-word")), }; }); await page.setViewportSize({ width: 1440, height: 920 }); await page.waitForTimeout(120); } codeQueueTaskCount = await page.locator('[data-testid^="codex-task-codex_"]').count(); if (wants("frontend:code-queue-initial-prompt-full-expand")) { codexInitialPromptFullMetrics = await page.evaluate(async () => { const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId=", { credentials: "same-origin" }); const tasksPayload = await tasksResponse.json().catch(() => null); const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; const candidate = tasks.find((task: any) => Array.isArray(task?.referenceTaskIds) && task.referenceTaskIds.length > 0); if (!candidate?.id) return { candidateFound: false }; const metaResponse = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(String(candidate.id))}?meta=1`, { credentials: "same-origin" }); const metaPayload = await metaResponse.json().catch(() => null); const fullPrompt = String(metaPayload?.task?.prompt || ""); const displayPrompt = String(metaPayload?.task?.displayPrompt || ""); return { candidateFound: true, taskId: String(candidate.id), promptChars: fullPrompt.length, displayPromptChars: displayPrompt.length, hasResolvedReference: fullPrompt.includes("# Code Queue 已解析引用上下文"), hasCurrentTaskMarker: fullPrompt.includes("# 本次任务"), hasReferenceTaskId: /codex_\\d+_[A-Za-z0-9_-]+/u.test(fullPrompt), }; }); if (codexInitialPromptFullMetrics.candidateFound) { const refTaskCard = page.getByTestId(`codex-task-${codexInitialPromptFullMetrics.taskId}`); await refTaskCard.scrollIntoViewIfNeeded({ timeout: 10000 }); await refTaskCard.click(); await page.waitForSelector('[data-testid="codex-initial-prompt-full"]', { timeout: 15000 }); codexInitialPromptFullMetrics.initialDefaultOpen = await page.getByTestId("codex-initial-prompt-full").evaluate((element) => (element as HTMLDetailsElement).open); await page.locator('[data-testid="codex-initial-prompt-full"] summary').click(); await page.waitForFunction(() => (document.querySelector('[data-testid="codex-initial-prompt-full"]') as HTMLDetailsElement | null)?.open === true, undefined, { timeout: 5000 }); const initialFullText = await page.getByTestId("codex-initial-prompt-full-text").innerText({ timeout: 5000 }); codexInitialPromptFullMetrics.initialExpanded = await page.getByTestId("codex-initial-prompt-full").evaluate((element) => (element as HTMLDetailsElement).open); codexInitialPromptFullMetrics.initialFullHasReference = initialFullText.includes("# Code Queue 已解析引用上下文") || initialFullText.includes("引用 Code Queue"); codexInitialPromptFullMetrics.initialFullHasCurrentTask = initialFullText.includes("# 本次任务") || initialFullText.includes("本次任务:"); codexInitialPromptFullMetrics.initialFullChars = initialFullText.length; codexInitialPromptFullMetrics.legacyPromptPanelMissing = await page.evaluate(() => document.querySelector(".codex-prompt-panel") === null && document.querySelector('[data-testid="codex-task-prompt-detail"]') === null && document.querySelector('[data-testid="codex-final-prompt-full"]') === null, ); } } if (wants("frontend:code-queue-trace-full-load")) { codexTraceFullMetrics = await page.evaluate(async () => { const tasksResponse = await fetch("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId=", { credentials: "same-origin" }); const tasksPayload = await tasksResponse.json().catch(() => null); const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; const terminal = new Set(["succeeded", "failed", "canceled"]); for (const task of tasks) { const taskId = String(task?.id || ""); if (!taskId || !terminal.has(String(task?.status || "")) || Number(task?.outputCount || 0) < 20) continue; const transcriptResponse = await fetch(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/transcript?afterSeq=0&limit=120&fullText=1`, { credentials: "same-origin" }); const transcriptPayload = await transcriptResponse.json().catch(() => null); const transcript = Array.isArray(transcriptPayload?.transcript) ? transcriptPayload.transcript : []; const toolCount = transcript.filter((line: any) => ["ran", "explored", "edited"].includes(String(line?.kind || ""))).length; if (toolCount >= 8) { return { candidateFound: true, taskId, apiTotal: Number(transcriptPayload?.total || transcript.length), apiToolCount: toolCount, apiHasInitialPrompt: transcript.some((line: any) => String(line?.title || "") === "Submitted prompt"), }; } } return { candidateFound: false, taskCount: tasks.length }; }); if (codexTraceFullMetrics.candidateFound) { for (let index = 0; index < 8; index += 1) { if (await page.getByTestId(`codex-task-${codexTraceFullMetrics.taskId}`).count()) break; const moreButton = page.getByTestId("codex-load-more-tasks-button"); if (!(await moreButton.count())) break; await moreButton.scrollIntoViewIfNeeded().catch(() => undefined); await moreButton.click(); await page.waitForTimeout(500); } const traceTaskCard = page.getByTestId(`codex-task-${codexTraceFullMetrics.taskId}`); await traceTaskCard.scrollIntoViewIfNeeded({ timeout: 10000 }); await traceTaskCard.click(); await page.waitForFunction(() => { const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null; const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const toolCount = output?.querySelectorAll('.codex-transcript-item.ran, .codex-transcript-item.explored, .codex-transcript-item.edited').length ?? 0; return pageElement?.getAttribute("data-load-state") === "complete" && toolCount >= 8; }, undefined, { timeout: 30000 }); codexTraceFullMetrics = { ...codexTraceFullMetrics, ...(await page.evaluate(() => { const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null; const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null; const text = output?.textContent || ""; return { uiItemCount: output?.querySelectorAll('.codex-transcript-item').length ?? 0, uiToolCount: output?.querySelectorAll('.codex-transcript-item.ran, .codex-transcript-item.explored, .codex-transcript-item.edited').length ?? 0, uiHasInitialPrompt: text.includes("Submitted prompt"), uiHasToolTrace: /Ran|Explored|Edited|Tool calls/.test(text), loadState: pageElement?.getAttribute("data-load-state") || "", loadPartial: pageElement?.getAttribute("data-load-partial") || "", loadRows: Number(pageElement?.getAttribute("data-load-transcript-rows") || 0), loadButtonVisible: document.querySelector('[data-testid="codex-load-full-trace-button"]') !== null, }; })), }; } } if (wants("frontend:code-queue-error-red-markers")) { const authCookies = await page.context().cookies(urls.frontendUrl); const cookieHeader = authCookies.map(({ name, value }) => `${name}=${value}`).join("; "); const authHeaders: Record = { accept: "application/json" }; if (cookieHeader) authHeaders.cookie = cookieHeader; const fetchJson = async (path: string): Promise => { const response = await fetch(new URL(path, urls.frontendUrl), { headers: authHeaders }); return response.json().catch(() => null); }; const tasksPayload = await fetchJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=120&transcriptLimit=0&compact=1&selected=0&includeActive=1&stats=0&afterSeq=0&preferId="); const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : []; const prioritized = tasks .filter((task: any) => ["failed", "canceled", "running", "judging", "succeeded"].includes(String(task?.status || ""))) .concat(tasks); for (const task of prioritized) { const taskId = String(task?.id || ""); if (!taskId) continue; const summaryPayload = await fetchJson(`/api/microservices/code-queue/proxy/api/tasks/${encodeURIComponent(taskId)}/trace-summary`); const summary = summaryPayload?.summary || null; const attempts = Array.isArray(summary?.attempts) ? summary.attempts : []; const errorAttempt = attempts.find((attempt: any) => Number(attempt?.errorCount ?? 0) > 0) || null; const errorCount = Number(errorAttempt?.errorCount ?? summary?.errorCount ?? 0); const attemptIndex = Number(errorAttempt?.index ?? 0); if (Number.isFinite(errorCount) && errorCount > 0 && Number.isFinite(attemptIndex) && attemptIndex > 0) { codeQueueErrorHighlightMetrics = { checked: true, candidateFound: true, taskId, errorCount, attemptIndex, status: String(task?.status || summary?.status || ""), }; break; } } if (!codeQueueErrorHighlightMetrics) { codeQueueErrorHighlightMetrics = { checked: true, candidateFound: false, taskCount: tasks.length }; } if (codeQueueErrorHighlightMetrics.candidateFound) { const errorTaskSearch = page.getByTestId("codex-task-search-input"); if (await errorTaskSearch.count()) { await errorTaskSearch.fill(codeQueueErrorHighlightMetrics.taskId); await page.waitForTimeout(1000); } for (let index = 0; index < 8; index += 1) { if (await page.getByTestId(`codex-task-${codeQueueErrorHighlightMetrics.taskId}`).count()) break; const moreButton = page.getByTestId("codex-load-more-tasks-button"); if (!(await moreButton.count())) break; await moreButton.scrollIntoViewIfNeeded().catch(() => undefined); await moreButton.click(); await page.waitForTimeout(500); } const errorTaskCard = page.getByTestId(`codex-task-${codeQueueErrorHighlightMetrics.taskId}`); await errorTaskCard.scrollIntoViewIfNeeded({ timeout: 10000 }); await errorTaskCard.click(); await page.waitForSelector('[data-testid="codex-execution-summary"]', { timeout: 15000 }); const errorSummaryTestId = Number(codeQueueErrorHighlightMetrics.attemptIndex || 0) > 1 ? `codex-execution-summary-attempt-${Number(codeQueueErrorHighlightMetrics.attemptIndex)}` : "codex-execution-summary"; const errorPillTestId = `${errorSummaryTestId}-error-count`; const errorSummaryLocator = page.getByTestId(errorSummaryTestId); const errorPillLocator = page.getByTestId(errorPillTestId); await errorPillLocator.waitFor({ state: "visible", timeout: 30000 }); const collapsedMetrics = await page.evaluate(({ summaryTestId, pillTestId }) => { const summary = document.querySelector(`[data-testid="${summaryTestId}"]`) as HTMLDetailsElement | null; const pill = document.querySelector(`[data-testid="${pillTestId}"]`) as HTMLElement | null; const pillStyle = pill ? getComputedStyle(pill) : null; const dangerColor = /207,\s*106,\s*84/u; return { summaryOpen: Boolean(summary?.open), pillVisible: Boolean(pill && pill.offsetParent !== null), pillText: String(pill?.textContent || "").trim(), pillClass: String(pill?.className || ""), pillColor: pillStyle?.color || "", pillBorderColor: pillStyle?.borderColor || "", pillBackgroundColor: pillStyle?.backgroundColor || "", hasDangerColor: dangerColor.test(pillStyle?.color || "") && dangerColor.test(pillStyle?.borderColor || ""), }; }, { summaryTestId: errorSummaryTestId, pillTestId: errorPillTestId }); await errorSummaryLocator.locator("summary").click(); await page.waitForFunction((testId) => (document.querySelector(`[data-testid="${testId}"]`) as HTMLDetailsElement | null)?.open === true, errorSummaryTestId, { timeout: 5000 }); await page.waitForFunction((testId) => document.querySelectorAll(`[data-testid="${testId}"] .codex-trace-step.error`).length > 0, errorSummaryTestId, { timeout: 15000 }); const expandedMetrics = await page.evaluate(({ summaryTestId }) => { const summary = document.querySelector(`[data-testid="${summaryTestId}"]`) as HTMLDetailsElement | null; const errorSteps = Array.from(summary?.querySelectorAll(".codex-trace-step.error") || []) as HTMLElement[]; const first = errorSteps[0] || null; const channel = first?.querySelector(".codex-output-channel") as HTMLElement | null; const code = first?.querySelector("code") as HTMLElement | null; const firstStyle = first ? getComputedStyle(first) : null; const channelStyle = channel ? getComputedStyle(channel) : null; const codeStyle = code ? getComputedStyle(code) : null; const dangerColor = /207,\s*106,\s*84/u; return { errorStepCount: errorSteps.length, firstStepClass: String(first?.className || ""), firstStepBorderColor: firstStyle?.borderColor || "", firstStepBackgroundColor: firstStyle?.backgroundColor || "", firstChannelColor: channelStyle?.color || "", firstCodeColor: codeStyle?.color || "", hasDangerColor: dangerColor.test(firstStyle?.borderColor || "") && dangerColor.test(channelStyle?.color || "") && dangerColor.test(codeStyle?.color || ""), }; }, { summaryTestId: errorSummaryTestId }); codeQueueErrorHighlightMetrics = { ...codeQueueErrorHighlightMetrics, collapsed: collapsedMetrics, expanded: expandedMetrics, ok: collapsedMetrics.pillVisible === true && collapsedMetrics.pillText.startsWith("Error ") && collapsedMetrics.hasDangerColor === true && collapsedMetrics.summaryOpen === false && expandedMetrics.errorStepCount > 0 && expandedMetrics.firstStepClass.includes("error") && expandedMetrics.hasDangerColor === true, }; } } if (wants("frontend:code-queue-stats-visible")) { codeQueueStatsVisibleMetrics = await runCodeQueueStatsFixture(page, urls.frontendUrl, "visible"); } if (wants("frontend:code-queue-stats-degraded-visible")) { codeQueueStatsDegradedMetrics = await runCodeQueueStatsFixture(page, urls.frontendUrl, "degraded"); } if (wants("frontend:code-queue-split-brain-live-not-failed")) { codeQueueSplitBrainLiveMetrics = await runCodeQueueSplitBrainFixture(page, urls.frontendUrl, "live"); } if (wants("frontend:code-queue-split-brain-stale-failed")) { codeQueueSplitBrainStaleMetrics = await runCodeQueueSplitBrainFixture(page, urls.frontendUrl, "stale"); } codeQueueOutputText = needCodeQueueFullSurface ? await page.locator('[data-testid="codex-output"]').innerText({ timeout: 5000 }) : ""; codeQueueText = await page.locator('[data-testid="code-queue-page"]').innerText({ timeout: 5000 }); codeQueueHtmlGuard = await page.evaluate(async () => { const root = document.getElementById("root"); const overviewAttr = root?.getAttribute("data-codex-overview") || ""; const response = await fetch(window.location.pathname, { credentials: "same-origin" }); const html = await response.text(); return { rootAttrLength: overviewAttr.length, rootAttrMissing: overviewAttr.length === 0, sourceBytes: html.length, sourceAttrMissing: !html.includes("data-codex-overview"), sourceNoBasePrompt: !html.includes("basePrompt"), }; }); codeQueueOptions = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => options.map((option) => (option as HTMLOptionElement).textContent || "")); codeQueueSubmitQueueControl = await page.evaluate(() => { const select = document.querySelector('[data-testid="code-queue-id-select"]') as HTMLSelectElement | null; const button = document.querySelector('[data-testid="codex-create-queue-button"]') as HTMLButtonElement | null; const mergeButton = document.querySelector('[data-testid="codex-merge-queue-button"]') as HTMLButtonElement | null; const prompt = document.querySelector('[data-testid="code-queue-task-form"] textarea') as HTMLTextAreaElement | null; const provider = document.querySelector('[data-testid="codex-provider-select"]') as HTMLSelectElement | null; const cwd = document.querySelector('[data-testid="codex-cwd-input"]') as HTMLInputElement | null; const cwdSelect = document.querySelector('[data-testid="codex-cwd-select"]') as HTMLSelectElement | null; const maxAttempts = document.querySelector('[data-testid="codex-max-attempts-input"]') as HTMLInputElement | null; const moveSelect = document.querySelector('[data-testid="codex-task-queue-move-select"]') as HTMLSelectElement | null; const moveButton = document.querySelector('[data-testid="codex-task-queue-move-button"]') as HTMLButtonElement | null; return { tagName: select?.tagName.toLowerCase() || "", optionCount: select?.options.length ?? 0, createButtonVisible: Boolean(button && button.offsetParent !== null), mergeButtonVisible: Boolean(mergeButton && mergeButton.offsetParent !== null), mergeButtonDisabled: Boolean(mergeButton?.disabled), mergeSourceInlineMissing: document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]') === null, mergeDialogMissingBeforeClick: document.querySelector('[data-testid="codex-merge-queue-dialog"]') === null, oldInputMissing: document.querySelector('[data-testid="code-queue-id-input"]') === null, promptDefaultEmpty: (prompt?.value || "") === "", providerValue: provider?.value || "", providerOptions: Array.from(provider?.options || []).map((option) => ({ value: option.value, text: option.textContent || "" })), cwdValue: cwd?.value || "", workdirSelectValue: cwdSelect?.value || "", workdirSelectOptionTexts: Array.from(cwdSelect?.options || []).map((option) => option.textContent || ""), maxAttemptsMax: maxAttempts?.max || "", maxAttemptsValue: maxAttempts?.value || "", moveQueueVisible: Boolean(moveSelect && moveSelect.offsetParent !== null && moveButton && moveButton.offsetParent !== null), }; }); codeQueuePromptDefaultEmpty = Boolean(codeQueueSubmitQueueControl.promptDefaultEmpty); if (codeQueueSubmitQueueControl.mergeButtonVisible && !codeQueueSubmitQueueControl.mergeButtonDisabled) { await page.getByTestId("codex-merge-queue-button").click(); await page.waitForSelector('[data-testid="codex-merge-queue-dialog"]', { timeout: 5000 }); const mergeDialogMetrics = await page.evaluate(() => { const dialog = document.querySelector('[data-testid="codex-merge-queue-dialog"]') as HTMLElement | null; const select = document.querySelector('[data-testid="codex-merge-queue-dialog"] [data-testid="codex-merge-source-queue-select"]') as HTMLSelectElement | null; const text = dialog?.textContent || ""; return { mergeDialogVisible: Boolean(dialog && dialog.offsetParent !== null), mergeDialogSelectVisible: Boolean(select && select.offsetParent !== null), mergeDialogSourceOptionCount: select?.options.length ?? 0, mergeDialogSelectInsideSubmitForm: Boolean(document.querySelector('[data-testid="code-queue-task-form"] [data-testid="codex-merge-source-queue-select"]')), mergeDialogUsesCommonComponent: Boolean(dialog?.classList.contains("unidesk-dialog")), mergeDialogDeleteNoteVisible: text.includes("自动删除源 queue"), }; }); codeQueueSubmitQueueControl = { ...codeQueueSubmitQueueControl, ...mergeDialogMetrics }; await page.getByTestId("codex-merge-queue-cancel").click(); await page.waitForSelector('[data-testid="codex-merge-queue-dialog"]', { state: "detached", timeout: 5000 }).catch(() => undefined); } codeQueueSwitchMetrics = await page.locator('[data-testid="code-queue-filter-select"] option').evaluateAll((options) => ({ optionCount: options.length, queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"), switched: false, })); if (codeQueueSwitchMetrics.queueValues.length > 0) { const targetQueueId = String(codeQueueSwitchMetrics.queueValues.find((value: string) => value !== "default") || codeQueueSwitchMetrics.queueValues[0]); await page.getByTestId("code-queue-filter-select").selectOption(targetQueueId); await page.waitForFunction((queueId) => { const text = document.body.innerText; return text.includes(`view=${queueId}`) || text.includes(`${queueId} ·`); }, targetQueueId, { timeout: 10000 }); await page.waitForFunction((queueId) => { const cards = Array.from(document.querySelectorAll('[data-testid^="codex-task-codex_"]')).map((node) => (node as HTMLElement).innerText); return cards.length === 0 || cards.every((text) => text.includes(`queue=${queueId}`)); }, targetQueueId, { timeout: 15000 }); codeQueueSwitchMetrics = { ...codeQueueSwitchMetrics, targetQueueId, switched: true }; await page.getByTestId("code-queue-filter-select").selectOption("__all__"); } } if (needCodeQueueRetryTraceFixture) { codeQueueRetryTraceFixtureMetrics = await runCodeQueueRetryTraceFixture(page, urls.frontendUrl); } if (needClaudeqq) { await page.getByRole("button", { name: /ClaudeQQ/ }).click(); await page.waitForSelector('[data-testid="claudeqq-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); return lower.includes("claudeqq 工作台") && text.includes("D601") && text.includes("QQ 事件订阅") && text.includes("消息推送") && lower.includes("napcat 容器登录") && text.includes("事件缓存") && text.includes("仅 UniDesk frontend 代理访问"); }, undefined, { timeout: 30000 }); await page.waitForFunction(() => document.body.innerText.includes("最近 QQ 事件"), undefined, { timeout: 10000 }); claudeqqText = await page.locator('[data-testid="claudeqq-page"]').innerText({ timeout: 5000 }); } if (needRouteDeepLink) { await page.goto(`${urls.frontendUrl}/app/pipeline/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="pipeline-page"]', { timeout: 15000 }); routeInitialPath = new URL(page.url()).pathname; routeDeepLinkText = await page.locator('[data-testid="pipeline-page"]').innerText({ timeout: 5000 }); await page.getByRole("button", { name: /资源节点/ }).click(); await page.getByRole("button", { name: /Docker 状态/ }).click(); await page.waitForSelector('[data-testid="docker-status-page"]', { timeout: 10000 }); routeDockerPath = new URL(page.url()).pathname; await page.goBack({ waitUntil: "domcontentloaded" }); routeBackIntermediatePath = new URL(page.url()).pathname; if (routeBackIntermediatePath !== "/app/pipeline/") { await page.goBack({ waitUntil: "domcontentloaded" }); } await page.waitForSelector('[data-testid="pipeline-page"]', { timeout: 15000 }); routeBackPath = new URL(page.url()).pathname; await page.goto(`${urls.frontendUrl}/ops/status/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="overview-page"]', { timeout: 10000 }); routeOverviewPath = new URL(page.url()).pathname; routeOverviewText = await page.locator('[data-testid="overview-page"]').innerText({ timeout: 5000 }); await page.goto(`${urls.frontendUrl}/app/code-queue/`, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="code-queue-page"]', { timeout: 15000 }); routeCodexPath = new URL(page.url()).pathname; routeCodexShellMetrics = await page.evaluate(() => ({ appShell: Boolean(document.querySelector('[data-testid="app-shell"]')), standalone: Boolean(document.querySelector('[data-testid="code-queue-standalone"]')), railText: document.querySelector(".rail")?.textContent || "", topbar: Boolean(document.querySelector(".topbar")), tabsText: document.querySelector(".tabs")?.textContent || "", codexPage: Boolean(document.querySelector('[data-testid="code-queue-page"]')), })); await page.locator('.rail [role="button"][title="用户服务"]').click(); await page.getByRole("button", { name: /^服务目录$/ }).click(); await page.waitForSelector('[data-testid="microservice-catalog-page"]', { timeout: 10000 }); } if (needPipeline) { await page.getByRole("button", { name: "Pipeline", exact: true }).click(); await page.waitForSelector('[data-testid="pipeline-page"]', { timeout: 10000 }); await page.waitForSelector('[data-testid="pipeline-react-flow"] .react-flow__node', { timeout: 30000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); return lower.includes("pipeline v2 工作台") && text.includes("控制图") && /epoch\s+甘特图/i.test(text) && text.includes("运行材料索引") && /Health\s+OK/i.test(text) && /组件\s+\d+/.test(text) && /运行记录\s+[1-9]\d*/.test(text); }, undefined, { timeout: 30000 }); pipelineFlowNodeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__node').count(); await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-react-flow"] .react-flow__edge').length > 0, undefined, { timeout: 10000 }).catch(() => undefined); pipelineFlowEdgeCount = await page.locator('[data-testid="pipeline-react-flow"] .react-flow__edge').count(); pipelineSelectedId = await page.locator('[data-testid="pipeline-select"]').evaluate((element) => (element as HTMLSelectElement).value); await page.waitForFunction(() => { const panel = document.querySelector('[data-testid="pipeline-oa-event-flow-panel"]') as HTMLElement | null; const text = panel?.innerText || ""; const lower = text.toLowerCase(); return lower.includes("oa flow") && text.includes("100%") && lower.includes("no-audit") && lower.includes("monitor 审核") && text.includes("禁止残留") && text.includes("policy-in-detail 0"); }, undefined, { timeout: 30000 }); pipelineText = await page.locator('[data-testid="pipeline-page"]').innerText({ timeout: 5000 }); pipelineOaPanelText = await page.locator('[data-testid="pipeline-oa-event-flow-panel"]').innerText({ timeout: 5000 }); pipelineMinimaxQuotaText = await page.locator('[data-testid="pipeline-minimax-quota-panel"]').innerText({ timeout: 5000 }); const pipelinePanelOrder = async () => page.locator('[data-testid="pipeline-page"] .pipeline-grid > .panel .panel-head h2').evaluateAll((elements) => elements.map((element) => (element as HTMLElement).innerText.trim()).filter(Boolean)); pipelinePriorityOrder.desktop = await pipelinePanelOrder(); await page.setViewportSize({ width: 390, height: 860 }); await page.waitForTimeout(120); pipelinePriorityOrder.mobile = await pipelinePanelOrder(); await page.setViewportSize({ width: 1440, height: 920 }); await page.waitForTimeout(120); pipelineGanttScaleLabel = await page.locator('[data-testid="pipeline-gantt-scale-label"]').innerText({ timeout: 5000 }); pipelineGanttAutoHideIdleChecked = await page.locator('[data-testid="pipeline-gantt-auto-hide-idle"]').isChecked(); await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-head-node"]').length > 0, undefined, { timeout: 15000 }); pipelineGanttHeaderNodeOrder = await page.locator('[data-testid="pipeline-gantt-head-node"]').evaluateAll((elements) => elements.map((element) => element.getAttribute("data-node-id") || "").filter(Boolean)); pipelineSidebarMetrics.nodeDefaultOpen = await page.getByTestId("pipeline-control-shell").getAttribute("data-sidebar-open") || ""; const firstFlowNode = page.locator('[data-testid="pipeline-react-flow"] .react-flow__node').first(); await firstFlowNode.scrollIntoViewIfNeeded({ timeout: 10000 }); await firstFlowNode.click({ force: true }); const firstNodeClickOpened = await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-control-shell"]')?.getAttribute("data-sidebar-open") === "true", undefined, { timeout: 2500 }) .then(() => true) .catch(() => false); if (!firstNodeClickOpened) { const box = await firstFlowNode.boundingBox(); if (box) await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); } await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-control-shell"]')?.getAttribute("data-sidebar-open") === "true", undefined, { timeout: 15000 }); await page.waitForSelector('[data-testid="pipeline-node-control"]', { timeout: 5000 }); pipelineSidebarMetrics.nodeAfterClickOpen = await page.getByTestId("pipeline-control-shell").getAttribute("data-sidebar-open") || ""; await page.getByTestId("pipeline-node-sidebar-collapse").click(); await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-control-shell"]')?.getAttribute("data-sidebar-open") === "false", undefined, { timeout: 10000 }); pipelineSidebarMetrics.nodeAfterCollapseOpen = await page.getByTestId("pipeline-control-shell").getAttribute("data-sidebar-open") || ""; pipelineSidebarMetrics.nodeToggleEnabledAfterCollapse = !(await page.getByTestId("pipeline-node-sidebar-toggle").isDisabled()); pipelineSidebarMetrics.ganttDefaultOpen = await page.getByTestId("pipeline-gantt-detail-layout").getAttribute("data-sidebar-open") || ""; const sidebarProbeGanttLines = page.locator('[data-testid="pipeline-epoch-gantt"] [data-testid="pipeline-gantt-line"]'); pipelineSidebarMetrics.ganttLineCount = await sidebarProbeGanttLines.count(); if (pipelineSidebarMetrics.ganttLineCount > 0) { const firstSidebarProbeGanttLine = sidebarProbeGanttLines.first(); await firstSidebarProbeGanttLine.scrollIntoViewIfNeeded({ timeout: 10000 }); await firstSidebarProbeGanttLine.click({ force: true }); const firstGanttClickOpened = await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-gantt-detail-layout"]')?.getAttribute("data-sidebar-open") === "true", undefined, { timeout: 2500 }) .then(() => true) .catch(() => false); if (!firstGanttClickOpened) { const box = await firstSidebarProbeGanttLine.boundingBox(); if (box) await page.mouse.click(box.x + box.width / 2, box.y + Math.max(2, Math.min(box.height / 2, 12))); } await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-gantt-detail-layout"]')?.getAttribute("data-sidebar-open") === "true", undefined, { timeout: 15000 }); await page.waitForSelector('[data-testid="pipeline-gantt-detail-panel"]', { timeout: 5000 }); pipelineSidebarMetrics.ganttAfterClickOpen = await page.getByTestId("pipeline-gantt-detail-layout").getAttribute("data-sidebar-open") || ""; await page.getByTestId("pipeline-gantt-sidebar-collapse").click(); await page.waitForFunction(() => document.querySelector('[data-testid="pipeline-gantt-detail-layout"]')?.getAttribute("data-sidebar-open") === "false", undefined, { timeout: 10000 }); pipelineSidebarMetrics.ganttAfterCollapseOpen = await page.getByTestId("pipeline-gantt-detail-layout").getAttribute("data-sidebar-open") || ""; pipelineSidebarMetrics.ganttToggleEnabledAfterCollapse = !(await page.getByTestId("pipeline-gantt-sidebar-toggle").isDisabled()); } await page.setViewportSize({ width: 390, height: 860 }); await page.waitForTimeout(120); pipelineSidebarMetrics.mobileNodeCollapsed = await page.getByTestId("pipeline-control-shell").getAttribute("data-sidebar-open") || ""; pipelineSidebarMetrics.mobileGanttCollapsed = await page.getByTestId("pipeline-gantt-detail-layout").getAttribute("data-sidebar-open") || ""; await page.setViewportSize({ width: 1440, height: 920 }); await page.waitForTimeout(120); if (wants("frontend:pipeline-gantt-frontend-y-accuracy")) { pipelineGanttFrontendYMetrics = await page.locator('[data-testid="pipeline-epoch-gantt"]').evaluate((element) => { const root = element as HTMLElement; const startMs = Number(root.dataset.startMs || "0"); const endMs = Number(root.dataset.endMs || "0"); const chartHeight = Number(root.dataset.chartHeight || "0"); const duration = Math.max(1, endMs - startMs); const expectedY = (ms: number): number => Math.max(0, Math.min(1, (ms - startMs) / duration)) * chartHeight; const violations: any[] = []; let checked = 0; let maxDelta = 0; const addCheck = (kind: string, id: string, actual: number, expected: number, tolerance = 1.25): void => { if (!Number.isFinite(actual) || !Number.isFinite(expected)) { violations.push({ kind, id, actual, expected, reason: "non-finite" }); return; } checked += 1; const delta = Math.abs(actual - expected); maxDelta = Math.max(maxDelta, delta); if (delta > tolerance) violations.push({ kind, id, actual: Math.round(actual * 1000) / 1000, expected: Math.round(expected * 1000) / 1000, delta: Math.round(delta * 1000) / 1000 }); }; const parseTop = (node: HTMLElement): number => Number.parseFloat(node.style.top || window.getComputedStyle(node).top || "NaN"); const parseHeight = (node: HTMLElement): number => Number.parseFloat(node.style.height || window.getComputedStyle(node).height || "NaN"); for (const tick of Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-tick"]')) as HTMLElement[]) { const ms = Number(tick.dataset.ms || "NaN"); const y = Number(tick.dataset.y || parseTop(tick)); const top = parseTop(tick); addCheck("tick-data-y", tick.dataset.ms || "", y, expectedY(ms)); addCheck("tick-style-top", tick.dataset.ms || "", top, expectedY(ms)); } for (const line of Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-line"]')) as HTMLElement[]) { const id = line.dataset.procedureRunId || line.dataset.nodeId || ""; const start = Number(line.dataset.startMs || "NaN"); const end = Number(line.dataset.endMs || "NaN"); const y1 = Number(line.dataset.y1 || "NaN"); const y2 = Number(line.dataset.y2 || "NaN"); const top = parseTop(line); const height = parseHeight(line); const expectedStartY = expectedY(start); const expectedEndY = expectedY(end); const expectedHeight = Math.max(line.dataset.live === "true" ? 24 : 10, expectedEndY - expectedStartY); addCheck("line-y1", id, y1, expectedStartY); addCheck("line-y2", id, y2, expectedEndY); addCheck("line-style-top", id, top, expectedStartY); addCheck("line-style-height", id, height, expectedHeight, 1.5); } for (const marker of Array.from(root.querySelectorAll('[data-marker-id]')) as HTMLElement[]) { const ms = Number(marker.dataset.ms || "NaN"); const y = Number(marker.dataset.y || parseTop(marker)); const top = parseTop(marker); addCheck("marker-data-y", marker.dataset.markerId || "", y, expectedY(ms)); addCheck("marker-style-top", marker.dataset.markerId || "", top, expectedY(ms)); } return { layoutSource: root.dataset.layoutSource || "", startMs, endMs, chartHeight, checked, maxDelta: Math.round(maxDelta * 1000) / 1000, violations: violations.slice(0, 12), tickCount: root.querySelectorAll('[data-testid="pipeline-gantt-tick"]').length, lineCount: root.querySelectorAll('[data-testid="pipeline-gantt-line"]').length, markerCount: root.querySelectorAll('[data-marker-id]').length, }; }); } pipelineSnapshotForFrontend = await page.evaluate(async () => { const response = await fetch("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3", { credentials: "same-origin" }); return response.ok ? response.json() : null; }); if (wants("frontend:pipeline-gantt-export")) { const exportButton = page.getByTestId("pipeline-export-gantt"); await exportButton.waitFor({ state: "visible", timeout: 10000 }); await page.waitForFunction(() => { const button = document.querySelector('[data-testid="pipeline-export-gantt"]') as HTMLButtonElement | null; return Boolean(button && !button.disabled); }, undefined, { timeout: 10000 }); const [download] = await Promise.all([ page.waitForEvent("download", { timeout: 15000 }), exportButton.click(), ]); const suggestedFilename = download.suggestedFilename(); const safeFilename = suggestedFilename.replace(/[^a-zA-Z0-9_.-]+/g, "-") || "pipeline-gantt.png"; const savePath = join(e2eDir, `${Date.now()}_${safeFilename}`); await download.saveAs(savePath); const bytes = readFileSync(savePath).byteLength; pipelineGanttExportInfo = { downloaded: true, suggestedFilename, savePath, bytes }; } if (wants("frontend:pipeline-step-timeline-visible")) { const regressionGanttLine = page.locator('[data-testid="pipeline-epoch-gantt"] [data-testid="pipeline-gantt-line"][data-node-id="repair-1"]').first(); const firstGanttLine = (await regressionGanttLine.count()) > 0 ? regressionGanttLine : page.locator('[data-testid="pipeline-epoch-gantt"] [data-testid="pipeline-gantt-line"]').first(); await firstGanttLine.scrollIntoViewIfNeeded({ timeout: 10000 }); const selectedGanttAttrs = await firstGanttLine.evaluate((element) => { const target = element as HTMLElement; return { selectedNodeId: target.dataset.nodeId || "", selectedProcedureRunId: target.dataset.procedureRunId || "", }; }); await firstGanttLine.click({ force: true }); await page.waitForSelector('[data-testid="pipeline-step-timeline"] [data-testid="pipeline-opencode-step-trace"]', { timeout: 30000 }); pipelineStepTimelineText = await page.locator('[data-testid="pipeline-step-timeline"]').innerText({ timeout: 5000 }); pipelineSessionHeadText = await page.locator('[data-testid="pipeline-step-timeline-session"]').innerText({ timeout: 5000 }); const pipelineTrace = page.locator('[data-testid="pipeline-opencode-step-trace"]').first(); const pipelineTraceHead = page.locator('[data-testid="pipeline-step-timeline"] .pipeline-trace-head').first(); firstPipelineStepSummaryText = await pipelineTraceHead.innerText({ timeout: 5000 }); pipelineTimelineMetrics = await page.locator('[data-testid="pipeline-step-timeline"]').evaluate((element, selected) => { const target = element as HTMLElement; const oldStepCards = Array.from(target.querySelectorAll('[data-testid="pipeline-opencode-step"], .pipeline-opencode-flow, .pipeline-opencode-step')) as HTMLElement[]; return { clientWidth: target.clientWidth, scrollWidth: target.scrollWidth, clientHeight: target.clientHeight, scrollHeight: target.scrollHeight, hasHorizontalScroll: target.scrollWidth > target.clientWidth + 1, flowConnectorVisible: false, maxStepIdleGapMs: 0, idleGapCount: 0, oldPipelineStepStyleCount: oldStepCards.length, emptyAttemptDetailCount: target.innerText.includes("暂无 attempt 详情") ? 1 : 0, selectedNodeId: selected.selectedNodeId, selectedProcedureRunId: selected.selectedProcedureRunId, }; }, selectedGanttAttrs); firstPipelineStepSummaryMetrics = await pipelineTrace.evaluate((element) => { const target = element as HTMLElement; return { clientWidth: target.clientWidth, scrollWidth: target.scrollWidth, clientHeight: target.clientHeight, scrollHeight: target.scrollHeight, hasHorizontalScroll: target.scrollWidth > target.clientWidth + 1, }; }); firstPipelineStepExpandedText = await pipelineTrace.innerText({ timeout: 5000 }); } if (wants("frontend:pipeline-gantt-observation-live-running")) { const candidate = await page.evaluate(async () => { const snapshotResponse = await fetch("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:2,runs:30", { credentials: "same-origin" }); if (!snapshotResponse.ok) return null; const snapshot = await snapshotResponse.json(); const runs = Array.isArray(snapshot?.runs) ? snapshot.runs : []; for (const run of runs) { const runId = String(run?.runId || ""); if (!runId) continue; const detailResponse = await fetch(`/api/microservices/pipeline/proxy/api/node-control/runs/${encodeURIComponent(runId)}?tail=160&view=timeline&_=${Date.now()}`, { credentials: "same-origin" }); if (!detailResponse.ok) continue; const detail = await detailResponse.json(); const hasObservation = JSON.stringify(detail).includes("node-long-running-observation"); const hasRunning = (Array.isArray(detail?.procedureRuns) ? detail.procedureRuns : []).some((procedure: any) => String(procedure?.status?.status || procedure?.artifact?.status || procedure?.status || "").toLowerCase() === "running"); if (hasObservation && hasRunning) return { pipelineId: String(run?.pipelineId || detail?.request?.pipelineId || ""), runId }; } return null; }); if (candidate?.pipelineId && candidate?.runId) { await page.locator('[data-testid="pipeline-select"]').selectOption(candidate.pipelineId); await page.waitForFunction((runId) => Array.from(document.querySelectorAll('[data-testid="pipeline-run-select"] option')).some((option) => (option as HTMLOptionElement).value === runId), candidate.runId, { timeout: 15000 }); await page.locator('[data-testid="pipeline-run-select"]').selectOption(candidate.runId); await page.waitForFunction((runId) => document.querySelector('[data-testid="pipeline-epoch-gantt"]')?.getAttribute("data-run-id") === runId, candidate.runId, { timeout: 15000 }); await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-observation-arrow"]').length > 0, undefined, { timeout: 15000 }); await page.waitForFunction(() => document.querySelectorAll('[data-testid="pipeline-gantt-line"][data-status="running"][data-live="true"]').length > 0, undefined, { timeout: 15000 }); await page.waitForTimeout(1200); pipelineObservationGanttMetrics = await page.locator('[data-testid="pipeline-epoch-gantt"]').evaluate((element, selectedCandidate) => { const root = element as HTMLElement; const arrows = Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-observation-arrow"]')); const liveBars = Array.from(root.querySelectorAll('[data-testid="pipeline-gantt-line"][data-status="running"][data-live="true"]')) as HTMLElement[]; const svg = root.querySelector(".pipeline-gantt-arrow-layer") as SVGSVGElement | null; const svgRect = svg?.getBoundingClientRect(); const markerElements = Array.from(root.querySelectorAll("[data-marker-id]")) as HTMLElement[]; const markerById = new Map(markerElements.map((marker) => [marker.getAttribute("data-marker-id") || "", marker])); return { candidate: selectedCandidate, observationArrowCount: arrows.length, observationSourceMarkerCount: root.querySelectorAll('[data-marker-id^="observation-source:"]').length, arrowPairs: arrows.map((arrow) => `${arrow.getAttribute("data-source-node-id") || ""}->${arrow.getAttribute("data-target-node-id") || ""}`), observationArrowTargetInsetsPx: arrows.map((arrow) => { const path = arrow as SVGPathElement; const targetMarker = markerById.get(path.getAttribute("data-target-marker-id") || ""); if (!svgRect || !targetMarker || typeof path.getTotalLength !== "function") return null; const end = path.getPointAtLength(path.getTotalLength()); const markerRect = targetMarker.getBoundingClientRect(); const markerCenterX = markerRect.left + markerRect.width / 2 - svgRect.left; const markerCenterY = markerRect.top + markerRect.height / 2 - svgRect.top; return Math.round(Math.hypot(markerCenterX - end.x, markerCenterY - end.y)); }).filter((value) => typeof value === "number"), liveRunningBarCount: liveBars.length, liveRunningHeights: liveBars.map((bar) => Math.round(bar.getBoundingClientRect().height)), runningAnimationNames: liveBars.map((bar) => window.getComputedStyle(bar).animationName), hasLiveSweep: liveBars.some((bar) => window.getComputedStyle(bar, "::after").animationName.includes("ganttLiveSweep")), }; }, candidate); pipelineObservationGanttMetrics = { ...pipelineObservationGanttMetrics, setup: pipelineObservationSetup }; } else { pipelineObservationGanttMetrics = { ...pipelineObservationGanttMetrics, candidate, setup: pipelineObservationSetup }; } } } if (needMetNonlinear) { await page.getByRole("button", { name: /MET Nonlinear/ }).click(); await page.waitForSelector('[data-testid="met-nonlinear-page"]', { timeout: 10000 }); await page.waitForFunction(() => { const text = document.body.innerText; const lower = text.toLowerCase(); return lower.includes("met nonlinear 训练编排") && text.includes("D601") && text.includes("Fork Project") && text.includes("加入待启动队列") && text.includes("启动队列") && text.includes("当前队列") && text.includes("GPU/镜像") && !text.includes("创建10个10轮任务") && text.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(text); }, undefined, { timeout: 30000 }); metNonlinearInitialText = await page.locator('[data-testid="met-nonlinear-page"]').innerText({ timeout: 5000 }); if (wants("frontend:met-nonlinear-project-tree-detail")) { await page.waitForSelector('[data-testid="met-project-tree"] .met-tree-row.project', { timeout: 30000 }); metProjectTreeText = await page.locator('[data-testid="met-project-tree"]').innerText({ timeout: 5000 }); await page.locator('[data-testid="met-project-tree"] .met-tree-row.project').first().click(); await page.waitForFunction(() => { const detail = document.querySelector('[data-testid="met-detail-panel"]') as HTMLElement | null; const text = detail?.innerText || ""; const lower = text.toLowerCase(); return lower.includes("project 详情") && lower.includes("config.json") && lower.includes("data/ 训练状态") && text.includes("模型参数") && text.includes("指标") && lower.includes("total params"); }, undefined, { timeout: 30000 }); metProjectDetailText = await page.locator('[data-testid="met-detail-panel"]').innerText({ timeout: 5000 }); } if (wants("frontend:met-nonlinear-queue-detail-speed")) { await page.getByTestId("met-tab-completed").click(); await page.waitForSelector('[data-testid^="met-job-row-"]', { timeout: 30000 }); metCompletedText = await page.locator('[data-testid="met-completed-pane"]').innerText({ timeout: 5000 }); await page.locator('[data-testid^="met-job-row-"]').first().click(); await page.waitForFunction(() => { const detail = document.querySelector('[data-testid="met-detail-panel"]') as HTMLElement | null; const text = detail?.innerText || ""; const lower = text.toLowerCase(); return text.includes("训练任务详情") && text.includes("任务状态") && text.includes("训练速度") && lower.includes("epoch/h") && lower.includes("config.json") && lower.includes("data/ 训练状态"); }, undefined, { timeout: 30000 }); metJobDetailText = await page.locator('[data-testid="met-detail-panel"]').innerText({ timeout: 5000 }); } } } if (needLayoutOverflowDesktop) { layoutOverflowDesktop = await collectLayoutOverflow(page, { width: 1440, height: 920 }, "desktop"); } if (needLayoutOverflowMobile) { layoutOverflowMobile = await collectLayoutOverflow(page, { width: 390, height: 860 }, "mobile"); await page.setViewportSize({ width: 1440, height: 920 }); } await page.screenshot({ path: screenshotPath, fullPage: true }); const microserviceCatalogTextLower = microserviceCatalogText.toLowerCase(); const todoNoteTextLower = todoNoteText.toLowerCase(); const findjobTextLower = findjobText.toLowerCase(); const decisionCenterTextLower = decisionCenterText.toLowerCase(); const codeQueueTextLower = codeQueueText.toLowerCase(); const claudeqqTextLower = claudeqqText.toLowerCase(); const pipelineTextLower = pipelineText.toLowerCase(); const activePipeline = Array.isArray(pipelineSnapshotForFrontend?.pipelines) ? pipelineSnapshotForFrontend.pipelines.find((pipeline: any) => String(pipeline?.id || "") === pipelineSelectedId) || pipelineSnapshotForFrontend.pipelines[0] : null; const expectedGanttNodeOrder = activePipeline ? pipelineSnapshotNodeOrder(activePipeline) : []; const headerIndexByNodeId = new Map(pipelineGanttHeaderNodeOrder.map((nodeId, index) => [nodeId, index])); const downstreamViolations = activePipeline ? pipelineSnapshotEdges(activePipeline).filter((edge) => edge.edgeType.toLowerCase() !== "rework" && headerIndexByNodeId.has(edge.source) && headerIndexByNodeId.has(edge.target) && Number(headerIndexByNodeId.get(edge.source)) >= Number(headerIndexByNodeId.get(edge.target))) : []; const metNonlinearTextLower = metNonlinearInitialText.toLowerCase(); if (needDevAuthApiCookie) { const devFrontendUrl = `http://${config.network.publicHost}:${config.network.devFrontend.port}`; const cookieJar = await page.context().cookies([devFrontendUrl]); const authContext = await browser.newContext({ viewport: { width: 1440, height: 920 } }); await authContext.addCookies(cookieJar); const devPage = await authContext.newPage(); devPage.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); devPage.on("pageerror", (error) => consoleErrors.push(error.message)); devPage.on("dialog", (dialog) => dialog.accept()); await devPage.goto(devFrontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 }); await devPage.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }).catch(() => undefined); await devPage.waitForTimeout(1200); const authenticatedDevResponse = await devPage.evaluate(async () => { const response = await fetch("/api/microservices", { credentials: "same-origin" }); const body = await response.json().catch(() => null); return { status: response.status, ok: response.ok, body, }; }); const devUiState = await devPage.evaluate(() => { const appShell = document.querySelector('[data-testid="app-shell"]'); const connText = document.querySelector('[data-testid="conn-text"]')?.textContent || ""; return { appShellVisible: Boolean(appShell), connText, }; }); await authContext.close(); const unauthenticatedDevResponse = await rawHttpProbe(`${devFrontendUrl}/api/microservices`, 15_000); devAuthApiMetrics = { checked: true, devOrigin: devFrontendUrl, productionSmoke: { frontendUrl: urls.frontendUrl, landedUrl, publicFrontendReached, }, authenticated: authenticatedDevResponse, ui: devUiState, unauthenticated: unauthenticatedDevResponse, }; } addSelectedCheck(checks, options, "frontend:login-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && bodyText.includes("核心在线"), { screenshotPath }); addSelectedCheck(checks, options, "frontend:dev-auth-api-cookie", devAuthApiMetrics.checked === true && devAuthApiMetrics.authenticated?.ok === true && devAuthApiMetrics.authenticated?.status !== 401 && Array.isArray(devAuthApiMetrics.authenticated?.body?.microservices) && devAuthApiMetrics.ui?.appShellVisible === true && String(devAuthApiMetrics.ui?.connText || "").includes("核心在线") && devAuthApiMetrics.productionSmoke?.publicFrontendReached === true && devAuthApiMetrics.unauthenticated?.status === 401 && String(devAuthApiMetrics.unauthenticated?.body?.error || "") === "authentication required", { devAuthApiMetrics }); addSelectedCheck(checks, options, "frontend:public-provider-info-visible", publicFrontendReached && bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && rawText.includes('"status": "online"') && rawText.includes(`"providerId": "${config.providerGateway.id}"`), { frontendUrl: urls.frontendUrl, landedUrl, providerId: config.providerGateway.id, rawTextPreview: rawText.slice(0, 400) }); addSelectedCheck(checks, options, "frontend:sidebar-collapse", railWidthBefore >= 160 && railWidthCollapsed <= 70, { railWidthBefore, railWidthCollapsed }); addSelectedCheck(checks, options, "frontend:mobile-nav-fixed-height", mobileRailHeights.length > 0 && mobileRailMax - mobileRailMin <= 1 && mobileRailMax <= 44, { mobileRailHeights }); addSelectedCheck(checks, options, "frontend:mobile-content-top-aligned", mobileContentMetrics.pageTop <= 190 && mobileContentMetrics.emptyTextOffset <= 14, { mobileContentMetrics }); addSelectedCheck(checks, options, "frontend:pending-task-drilldown", pendingTaskText.includes("待处理任务") && (pendingTaskText.includes("当前无待处理任务") || (pendingTaskText.toLowerCase().includes("provider") && pendingTaskText.includes("已等待"))), { pendingTaskPreview: pendingTaskText.slice(0, 600) }); addSelectedCheck(checks, options, "frontend:task-history-diagnostics", taskHistoryText.includes("任务耗时") && taskHistoryText.includes("诊断信息") && taskHistoryText.includes("失败原因") && taskHistoryText.includes("e2e forced failure for diagnostics"), { taskHistoryPreview: taskHistoryText.slice(0, 900) }); addSelectedCheck(checks, options, "frontend:no-naked-json-before-click", rawBlocksBefore === 0 && !nakedJsonText, { rawBlocksBefore, nakedJsonText }); addSelectedCheck(checks, options, "frontend:raw-json-explicit-button", rawText.includes('"providerId"') && rawText.includes(config.providerGateway.id), { rawTextPreview: rawText.slice(0, 400) }); addSelectedCheck(checks, options, "frontend:performance-panel-visible", performanceText.includes("性能面板") && performanceText.includes("Bwebui") && performanceText.includes("组件汇总") && performanceText.includes("最近失败请求") && performanceText.includes("内部操作汇总") && performanceText.includes("最近慢操作"), { performanceTextPreview: performanceText.slice(0, 1200) }); addSelectedCheck(checks, options, "frontend:system-monitor-visible", monitorText.includes("任务管理器视图") && monitorText.includes("CPU") && monitorText.includes("Memory") && monitorText.includes("Disk") && monitorText.includes("不含缓存") && monitorText.includes("进程资源占用"), { monitorTextPreview: monitorText.slice(0, 1000) }); addSelectedCheck(checks, options, "frontend:process-resource-sorting", processTableText.includes("进程") && processTableText.includes("PID") && processTableText.includes("CPU") && processTableText.includes("内存") && processTableText.includes("磁盘 I/O") && processMemorySortAria === "descending" && processDefaultMemoryDescending && processCpuSortAria === "descending" && processCpuDescending, { processMemorySortAria, processCpuSortAria, processMemoryValues: processMemoryValues.slice(0, 12), processCpuValues: processCpuValues.slice(0, 12), processTablePreview: processTableText.slice(0, 1000) }); addSelectedCheck(checks, options, "frontend:upgrade-plan-dispatch", upgradeControlText.includes("预检升级 已下发") && upgradeControlText.includes("指定 Provider") && upgradeControlText.includes(`v${providerGatewayPackageVersion()}`), { providerId: config.providerGateway.id, upgradeControlPreview: upgradeControlText.slice(0, 500) }); addSelectedCheck(checks, options, "frontend:docker-status-visible", dockerText.toLowerCase().includes("docker desktop 视图") && dockerText.toLowerCase().includes("containers") && dockerText.includes("unidesk_pgdata_10gb") && (dockerText.includes("unidesk-frontend") || dockerText.includes("unidesk-backend-core")), { dockerTextPreview: dockerText.slice(0, 800) }); addSelectedCheck(checks, options, "frontend:gateway-version-records-visible", gatewayTextLower.includes("provider gateway 版本") && gatewayText.includes("远程更新记录") && gatewayTextLower.includes("gateway 版本") && gatewayText.includes(config.providerGateway.id) && gatewayText.includes(`v${providerGatewayPackageVersion()}`) && gatewayTextLower.includes("provider.upgrade"), { gatewayTextPreview: gatewayText.slice(0, 900) }); addSelectedCheck(checks, options, "frontend:gateway-duration-subsecond-visible", gatewayHasSubsecondDuration && !gatewayHasRoundedZeroDuration, { gatewayHasSubsecondDuration, gatewayHasRoundedZeroDuration, gatewayTextPreview: gatewayText.slice(0, 900) }); addSelectedCheck(checks, options, "frontend:provider-operation-availability-visible", sshAvailabilityTexts.length >= 1 && upgradeAvailabilityTexts.length >= 1 && sshAvailabilityTexts.every((text) => text.includes("SSH 透传")) && upgradeAvailabilityTexts.every((text) => text.includes("远程更新")) && upgradeAvailabilityTexts.some((text) => text.includes("always-enabled")), { sshAvailabilityTexts, upgradeAvailabilityTexts }); addSelectedCheck(checks, options, "frontend:overview-pgdata-visible", bodyText.includes("PGDATA") && bodyText.includes(config.database.volume), { bodyPreview: bodyText.slice(0, 800) }); addSelectedCheck(checks, options, "frontend:microservice-catalog-visible", microserviceCatalogTextLower.includes("findjob") && microserviceCatalogTextLower.includes("pipeline") && microserviceCatalogTextLower.includes("todo note") && microserviceCatalogTextLower.includes("met nonlinear") && microserviceCatalogTextLower.includes("claudeqq") && microserviceCatalogTextLower.includes("oa event flow") && microserviceCatalogTextLower.includes("code queue") && microserviceCatalogTextLower.includes("decision center") && microserviceCatalogText.includes("D601") && microserviceCatalogText.includes(config.providerGateway.id) && microserviceCatalogTextLower.includes("private") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/findjob") && microserviceCatalogText.includes("https://github.com/pikasTech/pipeline") && microserviceCatalogText.includes("https://github.com/pikasTech/met_nonlinear") && microserviceCatalogText.includes("https://gitee.com/lyon1998/agent_skills") && microserviceCatalogText.includes("https://gitee.com/Lyon1998/todo_note") && microserviceCatalogText.includes("https://github.com/pikasTech/unidesk"), { microserviceCatalogPreview: microserviceCatalogText.slice(0, 2000) }); addSelectedCheck(checks, options, "frontend:todo-note-integrated-visible", todoNoteTextLower.includes("todo note 工作台") && todoNoteText.includes("CONSTAR") && todoNoteText.includes("大论文") && todoNoteText.includes("UI E2E smoke task") && todoNoteText.includes("撤销") && todoNoteText.includes("重做") && todoNoteText.includes("全部展开") && todoNoteText.includes("仅 UniDesk frontend 代理访问"), { todoNoteTextPreview: todoNoteText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:findjob-integrated-visible", findjobTextLower.includes("findjob 工作台".toLowerCase()) && findjobText.includes("岗位总量") && findjobText.includes("D601") && findjobText.includes("近期岗位") && findjobText.includes("仅 UniDesk frontend 代理访问") && /岗位总量\s+\d+/.test(findjobText) && /health\s+ok/i.test(findjobText) && /[1-9]\d*\/[1-9]\d*\s+preview/i.test(findjobText), { findjobTextPreview: findjobText.slice(0, 1200) }); addSelectedCheck(checks, options, "frontend:oa-event-flow-visible", oaEventFlowText.toLowerCase().includes("oa event flow") && oaEventFlowText.includes("事件表") && oaEventFlowText.includes("统计中心") && oaEventFlowText.includes("service:code-queue") && oaEventFlowText.includes("仅 UniDesk frontend 代理访问") && oaEventFlowMetrics.pageVisible === true && oaEventFlowMetrics.eventTableVisible === true && oaEventFlowMetrics.statsVisible === true && oaEventFlowMetrics.tagFilterValue === "service:code-queue" && Number(oaEventFlowMetrics.rawButtonCount || 0) >= 2 && !oaEventFlowText.includes("{\n"), { oaEventFlowMetrics, oaEventFlowTextPreview: oaEventFlowText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:decision-center-visible", decisionCenterTextLower.includes("decision center") && decisionCenterText.includes("G0/G1 目标") && decisionCenterText.includes("P0/P1 Blocker") && decisionCenterText.includes("停放事项") && decisionCenterText.includes("最近会议/决议") && decisionCenterText.includes("全部记录") && decisionCenterText.includes("PostgreSQL") && decisionCenterText.includes("查看原始JSON") && decisionCenterMetrics.pageVisible === true && decisionCenterMetrics.tableVisible === true && decisionCenterE2eRecord?.ok === true && decisionCenterDeleteResult?.ok === true && Number(decisionCenterMetrics.rawButtonCount || 0) >= 1 && Number(decisionCenterMetrics.rawJsonBlocks || 0) === 0 && Number(decisionCenterMetrics.chatInputCount || 0) === 0 && !decisionCenterText.includes("{\n"), { decisionCenterMetrics, decisionCenterE2eRecord, decisionCenterDeleteResult, decisionCenterTextPreview: decisionCenterText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:decision-center-demand-management-visible", decisionCenterDemandText.includes("需求管理工作区") && decisionCenterDemandText.includes("外部目标") && decisionCenterDemandText.includes("内部目标") && decisionCenterDemandText.includes("阻塞") && decisionCenterDemandText.includes("停放事项") && decisionCenterDemandText.includes("决议/实验/债务") && decisionCenterDemandText.includes("录入需求记录") && decisionCenterDemandMetrics.workspaceVisible === true && decisionCenterDemandMetrics.editorVisible === true && decisionCenterDemandMetrics.filtersVisible === true && Number(decisionCenterDemandMetrics.filterButtons || 0) >= 5 && decisionCenterDemandMetrics.lanes?.["external-goal"] === true && decisionCenterDemandMetrics.lanes?.["internal-goal"] === true && decisionCenterDemandMetrics.lanes?.blocker === true && decisionCenterDemandMetrics.lanes?.parked === true && decisionCenterDemandMetrics.lanes?.authority === true && !decisionCenterDemandText.includes("{\n"), { decisionCenterDemandMetrics, decisionCenterDemandPreview: decisionCenterDemandText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:decision-center-diary-visible", decisionCenterDiaryText.includes("工作日记编辑台") && decisionCenterDiaryText.includes("日记筛选") && decisionCenterDiaryText.includes("按天条目") && decisionCenterDiaryText.includes("PostgreSQL / YYYY-MM/YYYY-MM-DD.md") && decisionCenterDiaryMetrics.editorVisible === true && decisionCenterDiaryMetrics.todayButtonVisible === true && decisionCenterDiaryMetrics.dateValue === "2099-12-30" && String(decisionCenterDiaryMetrics.bodyValue || "").includes("E2E diary marker") && Number(decisionCenterDiaryMetrics.entryCards || 0) >= 1 && Number(decisionCenterDiaryMetrics.duplicateDateCards || 0) >= 1 && decisionCenterDiaryMetrics.selectedHasMarker === true && !decisionCenterDiaryText.includes("{\n"), { decisionCenterDiaryMetrics, decisionCenterDiaryPreview: decisionCenterDiaryText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:code-queue-integrated-visible", codeQueueTextLower.includes("code queue") && codeQueueText.includes("gpt-5.4-mini") && codeQueueText.includes("gpt-5.4") && codeQueueText.includes("gpt-5.5") && codeQueueText.includes("提交任务") && codeQueueText.includes("执行 Provider") && codeQueueText.includes("入队份数") && codeQueueText.includes("追加 prompt") && codeQueueText.includes("打断") && codeQueueTextLower.includes("查看 queue") && codeQueueText.includes("创建 queue") && codeQueueText.includes("合并 queue") && codeQueueOptions.some((text) => text.includes("All queues")) && codeQueueTracePlacement.firstChildIsTrace === true && codeQueueTracePlacement.noPageTopStatus === true && codeQueueTracePlacement.filterInsideTracePanel === true && codeQueueTracePlacement.taskSearchVisible === true && codeQueueTracePlacement.traceStatusVisible === true && codeQueueTracePlacement.markAllReadVisible === true && codeQueueGlobalStatus.activeMicroserviceVisible === true && codeQueueSidebarUpdateMetrics.hasRecentUpdateLabel === true && codeQueueHtmlGuard.rootAttrMissing === true && codeQueueHtmlGuard.sourceAttrMissing === true && codeQueueHtmlGuard.sourceNoBasePrompt === true && codeQueueSubmitQueueControl.tagName === "select" && codeQueueSubmitQueueControl.createButtonVisible === true && codeQueueSubmitQueueControl.mergeButtonVisible === true && codeQueueSubmitQueueControl.mergeSourceInlineMissing === true && codeQueueSubmitQueueControl.mergeDialogMissingBeforeClick === true && (codeQueueSubmitQueueControl.mergeButtonDisabled === true || (codeQueueSubmitQueueControl.mergeDialogVisible === true && codeQueueSubmitQueueControl.mergeDialogSelectVisible === true && Number(codeQueueSubmitQueueControl.mergeDialogSourceOptionCount || 0) > 1 && codeQueueSubmitQueueControl.mergeDialogSelectInsideSubmitForm !== true && codeQueueSubmitQueueControl.mergeDialogUsesCommonComponent === true && codeQueueSubmitQueueControl.mergeDialogDeleteNoteVisible === true)) && codeQueueSubmitQueueControl.oldInputMissing === true && codeQueueSubmitQueueControl.providerValue === "D601" && codeQueueSubmitQueueControl.cwdValue === "/workspace" && Array.isArray(codeQueueSubmitQueueControl.providerOptions) && codeQueueSubmitQueueControl.providerOptions.some((item: any) => item.value === "D601" && String(item.text || "").includes("/workspace")) && codeQueueSubmitQueueControl.maxAttemptsMax === "99" && codeQueueSubmitQueueControl.maxAttemptsValue === "99" && codeQueueSubmitQueueControl.moveQueueVisible === true && codeQueuePromptDefaultEmpty === true && codeQueueSubmitGuard.batchRowVisible === true && codeQueueSubmitGuard.checkboxVisible === true && codeQueueSubmitGuard.disabledBeforeConfirm === true && codeQueueSubmitGuard.enabledAfterConfirm === true && codeQueueSubmitGuard.waitElementMissingBeforeSubmit === true && codeQueueScrollbarMetrics.transcriptThin === true && codeQueueScrollbarMetrics.toolHorizontalHidden === true && (codeQueueSwitchMetrics.optionCount <= 1 || codeQueueSwitchMetrics.switched === true) && codeQueueTextLower.includes("attempts") && codeQueueText.includes("仅 UniDesk frontend 代理访问") && (codeQueueTaskCount === 0 || codeQueueOutputText.includes("Submitted prompt")), { codeQueueTaskCount, codeQueueOptions, codeQueueSwitchMetrics, codeQueueSubmitQueueControl, codeQueueSubmitGuard, codeQueueScrollbarMetrics, codeQueuePromptDefaultEmpty, codeQueueTracePlacement, codeQueueGlobalStatus, codeQueueSidebarUpdateMetrics, codeQueueHtmlGuard, codeQueueOutputPreview: codeQueueOutputText.slice(0, 900), codeQueueTextPreview: codeQueueText.slice(0, 1400) }); const expectedFrontendCodeQueueProvider = isDevFrontendTarget() ? "D601-dev" : "D601"; const expectedFrontendCodeQueueWorkdir = isDevFrontendTarget() ? "/workspace-dev" : "/workspace"; addSelectedCheck(checks, options, "frontend:code-queue-workdirs-loaded", codeQueueSubmitQueueControl.providerValue === expectedFrontendCodeQueueProvider && codeQueueSubmitQueueControl.cwdValue === expectedFrontendCodeQueueWorkdir && codeQueueSubmitQueueControl.workdirSelectValue === expectedFrontendCodeQueueWorkdir && Array.isArray(codeQueueSubmitQueueControl.workdirSelectOptionTexts) && codeQueueSubmitQueueControl.workdirSelectOptionTexts.some((text: string) => text.includes(expectedFrontendCodeQueueWorkdir)) && !codeQueueText.includes("加载工作目录失败") && !codeQueueText.includes("HTTP 404"), { expectedFrontendCodeQueueProvider, expectedFrontendCodeQueueWorkdir, codeQueueSubmitQueueControl, codeQueueTextPreview: codeQueueText.slice(0, 1200) }); addSelectedCheck(checks, options, "frontend:code-queue-enqueue-await-smoke", codeQueueEnqueueAwaitSmoke.checked === true && codeQueueEnqueueAwaitSmoke.delayedPostCount === 1 && codeQueueEnqueueAwaitSmoke.responseOk === true && codeQueueEnqueueAwaitSmoke.responseStatus === 202 && /^codex_\d+_[A-Za-z0-9_-]+$/u.test(String(codeQueueEnqueueAwaitSmoke.taskId || "")) && codeQueueEnqueueAwaitSmoke.submitQueueId === "e2e-submit-smoke" && codeQueueEnqueueAwaitSmoke.duringAwait?.formBusy === true && codeQueueEnqueueAwaitSmoke.duringAwait?.waitVisible === true && codeQueueEnqueueAwaitSmoke.duringAwait?.buttonDisabled === true && codeQueueEnqueueAwaitSmoke.duringAwait?.textareaDisabled === true && codeQueueEnqueueAwaitSmoke.afterAwait?.formBusy === false && codeQueueEnqueueAwaitSmoke.afterAwait?.waitMissing === true && codeQueueEnqueueAwaitSmoke.afterAwait?.textareaEmpty === true && codeQueueEnqueueAwaitSmoke.beforeSubmitView?.filterValue === "__all__" && codeQueueEnqueueAwaitSmoke.afterAwait?.filterValue === codeQueueEnqueueAwaitSmoke.beforeSubmitView?.filterValue && codeQueueEnqueueAwaitSmoke.afterAwait?.submitQueueValue === codeQueueEnqueueAwaitSmoke.beforeSubmitView?.submitQueueValue && codeQueueEnqueueAwaitSmoke.beforeSubmitView?.submitQueueValue === codeQueueEnqueueAwaitSmoke.submitQueueId && Number(codeQueueEnqueueAwaitSmoke.postSubmitSubmitQueueOverviewRequestCount || 0) === 0 && codeQueueEnqueueAwaitSmoke.afterAwait?.cardVisible === true && codeQueueEnqueueAwaitSmoke.afterAwait?.selectedTraceHasTask === true && codeQueueEnqueueAwaitSmoke.afterAwait?.allVisibleCardsRespectFilter === true && codeQueueEnqueueAwaitSmoke.storedTask?.ok === true && codeQueueEnqueueAwaitSmoke.storedTask?.id === codeQueueEnqueueAwaitSmoke.taskId && codeQueueEnqueueAwaitSmoke.storedTask?.queueId === codeQueueEnqueueAwaitSmoke.submitQueueId && codeQueueEnqueueAwaitSmoke.storedTask?.promptIncludesMarker === true && (codeQueueEnqueueAwaitSmoke.interrupt?.ok === true || codeQueueEnqueueAwaitSmoke.interrupt?.status === 409), { codeQueueEnqueueAwaitSmoke }); addSelectedCheck(checks, options, "frontend:code-queue-summary-mobile-wrap", codeQueueSummaryMobileMetrics.checked === true && (codeQueueSummaryMobileMetrics.summaryCount === 0 || codeQueueSummaryMobileMetrics.ok === true), { codeQueueSummaryMobileMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-long-prompt-observation", codeQueueLongPromptMetrics.checked === true && codeQueueLongPromptMetrics.createOk === true && /^codex_\d+_[A-Za-z0-9_-]+$/u.test(String(codeQueueLongPromptMetrics.taskId || "")) && Number(codeQueueLongPromptMetrics.promptChars || 0) > 2500 && codeQueueLongPromptMetrics.checks?.createOriginalHasTail === true && codeQueueLongPromptMetrics.checks?.overviewOriginalHasTail === true && codeQueueLongPromptMetrics.checks?.selectedOriginalHasTail === true && codeQueueLongPromptMetrics.checks?.metaOriginalHasTail === true && codeQueueLongPromptMetrics.checks?.initialPromptHasTail === true && codeQueueLongPromptMetrics.checks?.uiFullPromptHasTail === true && codeQueueLongPromptMetrics.checks?.originalsHaveNoTruncationMarker === true && codeQueueLongPromptMetrics.checks?.previewFieldsExplicit === true && codeQueueLongPromptMetrics.checks?.previewCanTruncate === true, { codeQueueLongPromptMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-error-red-markers", codeQueueErrorHighlightMetrics.checked === true && codeQueueErrorHighlightMetrics.candidateFound === true && codeQueueErrorHighlightMetrics.ok === true, { codeQueueErrorHighlightMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-initial-prompt-full-expand", codexInitialPromptFullMetrics.candidateFound === false || ( codexInitialPromptFullMetrics.promptChars > codexInitialPromptFullMetrics.displayPromptChars && codexInitialPromptFullMetrics.initialDefaultOpen === false && codexInitialPromptFullMetrics.initialExpanded === true && codexInitialPromptFullMetrics.initialFullHasReference === true && codexInitialPromptFullMetrics.initialFullHasCurrentTask === true && codexInitialPromptFullMetrics.legacyPromptPanelMissing === true ), { codexInitialPromptFullMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-trace-full-load", codexTraceFullMetrics.candidateFound === false || ( codexTraceFullMetrics.apiTotal >= 20 && codexTraceFullMetrics.apiToolCount >= 8 && codexTraceFullMetrics.apiHasInitialPrompt === true && codexTraceFullMetrics.uiItemCount >= 10 && codexTraceFullMetrics.uiToolCount >= 8 && codexTraceFullMetrics.uiHasInitialPrompt === true && codexTraceFullMetrics.uiHasToolTrace === true && codexTraceFullMetrics.loadState === "complete" && codexTraceFullMetrics.loadPartial !== "true" ), { codexTraceFullMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap", codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true, { codexJudgeWrapMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-stats-visible", codeQueueStatsVisibleMetrics.checked === true && codeQueueStatsVisibleMetrics.ok === true, { codeQueueStatsVisibleMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-stats-degraded-visible", codeQueueStatsDegradedMetrics.checked === true && codeQueueStatsDegradedMetrics.ok === true, { codeQueueStatsDegradedMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-split-brain-live-not-failed", codeQueueSplitBrainLiveMetrics.checked === true && codeQueueSplitBrainLiveMetrics.ok === true && codeQueueSplitBrainLiveMetrics.healthFailed === false && codeQueueSplitBrainLiveMetrics.health?.value === "split-brain live" && codeQueueSplitBrainLiveMetrics.health?.className.includes("degraded-live") && codeQueueSplitBrainLiveMetrics.effective?.value === "live" && codeQueueSplitBrainLiveMetrics.heartbeatRisk?.value === "0" && Number(codeQueueSplitBrainLiveMetrics.failedMetricCount || 0) === 0 && String(codeQueueSplitBrainLiveMetrics.panelText || "").includes("continue-supervision") && String(codeQueueSplitBrainLiveMetrics.panelText || "").includes("控制面/执行面观测分裂") && !/任务死亡|dead task|task death|必须重启|must restart/i.test(String(codeQueueSplitBrainLiveMetrics.panelText || "")), { codeQueueSplitBrainLiveMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-split-brain-stale-failed", codeQueueSplitBrainStaleMetrics.checked === true && codeQueueSplitBrainStaleMetrics.ok === true && codeQueueSplitBrainStaleMetrics.healthFailed === true && codeQueueSplitBrainStaleMetrics.effective?.value === "at-risk" && codeQueueSplitBrainStaleMetrics.heartbeatRisk?.value === "1" && codeQueueSplitBrainStaleMetrics.heartbeatRisk?.className.includes("failed") && codeQueueSplitBrainStaleMetrics.staleCandidates?.value === "1" && String(codeQueueSplitBrainStaleMetrics.panelText || "").includes("investigate-heartbeat-risk"), { codeQueueSplitBrainStaleMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.ok === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardAttemptVisible === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentAttempt === "2" && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentRunning === "true" && Number(codeQueueRetryTraceFixtureMetrics.currentStepRows || 0) >= 2 && (codeQueueRetryTraceFixtureMetrics.traceRequests || []).some((search: string) => String(search).includes("attempt=2")), { codeQueueRetryTraceFixtureMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-step-missing-diagnostic", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepText !== "STEP --" && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepText !== "STEP --" && ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepState || "")) && ["syncing", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepState || "")), { codeQueueRetryTraceFixtureMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-judge-feedback-attempt-order", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentBeforeJudge === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.judgeBeforeFeedback === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentJudgeMissing === true && String(codeQueueRetryTraceFixtureMetrics.feedbackText || "").includes("Continue with attempt 2") && (codeQueueRetryTraceFixtureMetrics.promptRequests || []).some((search: string) => String(search).includes("part=feedback") && String(search).includes("attempt=1")), { codeQueueRetryTraceFixtureMetrics }); addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("D601 k3s Service") && claudeqqText.includes("k3s://unidesk/claudeqq") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) }); addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible", pipelineTextLower.includes("pipeline v2 工作台".toLowerCase()) && pipelineText.includes("D601") && pipelineText.includes("控制图") && pipelineText.includes("评分器") && /epoch\s+甘特图/i.test(pipelineText) && pipelineText.includes("运行材料索引") && pipelineText.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(pipelineText) && /组件\s+\d+/.test(pipelineText) && /运行记录\s+[1-9]\d*/.test(pipelineText) && pipelinePriorityOrder.desktop[0] === "控制图" && /epoch\s+甘特图/i.test(String(pipelinePriorityOrder.desktop[1] || "")) && pipelinePriorityOrder.mobile[0] === "控制图" && /epoch\s+甘特图/i.test(String(pipelinePriorityOrder.mobile[1] || "")), { pipelinePriorityOrder, pipelineTextPreview: pipelineText.slice(0, 1200) }); addSelectedCheck(checks, options, "frontend:pipeline-react-flow-visible", pipelineFlowNodeCount > 0 && pipelineFlowEdgeCount > 0, { pipelineFlowNodeCount, pipelineFlowEdgeCount }); addSelectedCheck(checks, options, "frontend:pipeline-sidebars-collapsible", pipelineSidebarMetrics.nodeDefaultOpen === "false" && pipelineSidebarMetrics.nodeAfterClickOpen === "true" && pipelineSidebarMetrics.nodeAfterCollapseOpen === "false" && pipelineSidebarMetrics.nodeToggleEnabledAfterCollapse === true && pipelineSidebarMetrics.ganttDefaultOpen === "false" && Number(pipelineSidebarMetrics.ganttLineCount || 0) > 0 && pipelineSidebarMetrics.ganttAfterClickOpen === "true" && pipelineSidebarMetrics.ganttAfterCollapseOpen === "false" && pipelineSidebarMetrics.ganttToggleEnabledAfterCollapse === true && pipelineSidebarMetrics.mobileNodeCollapsed === "false" && pipelineSidebarMetrics.mobileGanttCollapsed === "false", { pipelineSidebarMetrics }); addSelectedCheck(checks, options, "frontend:pipeline-minimax-quota-visible", pipelineMinimaxQuotaText.includes("MiniMax") && pipelineMinimaxQuotaText.includes("当前窗口") && pipelineMinimaxQuotaText.includes("剩余额度") && pipelineMinimaxQuotaText.includes("重置时间") && !pipelineMinimaxQuotaText.includes("{"), { pipelineMinimaxQuotaPreview: pipelineMinimaxQuotaText.slice(0, 1000) }); addSelectedCheck(checks, options, "frontend:pipeline-oa-event-flow-visible", pipelineOaPanelText.toLowerCase().includes("oa flow") && pipelineOaPanelText.includes("100%") && pipelineOaPanelText.toLowerCase().includes("no-audit") && pipelineOaPanelText.toLowerCase().includes("monitor 审核") && pipelineOaPanelText.includes("禁止残留") && pipelineOaPanelText.includes("policy-in-detail 0") && !pipelineOaPanelText.includes("{"), { pipelineOaPanelPreview: pipelineOaPanelText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:pipeline-gantt-defaults", pipelineGanttScaleLabel.includes("100 px/min") && pipelineGanttAutoHideIdleChecked === false && pipelineGanttHeaderNodeOrder.length > 0 && downstreamViolations.length === 0, { pipelineGanttScaleLabel, pipelineGanttAutoHideIdleChecked, pipelineGanttHeaderNodeOrder, expectedGanttNodeOrder, downstreamViolations, }); addSelectedCheck(checks, options, "frontend:pipeline-gantt-frontend-y-accuracy", pipelineGanttFrontendYMetrics.layoutSource === "frontend-y" && Number(pipelineGanttFrontendYMetrics.checked || 0) > 0 && Number(pipelineGanttFrontendYMetrics.maxDelta || 0) <= 1.25 && (pipelineGanttFrontendYMetrics.violations || []).length === 0, { pipelineGanttFrontendYMetrics }); addSelectedCheck(checks, options, "frontend:pipeline-gantt-export", pipelineGanttExportInfo.downloaded === true && Number(pipelineGanttExportInfo.bytes || 0) > 2048 && /\.(png|svg)$/i.test(String(pipelineGanttExportInfo.suggestedFilename || "")), { pipelineGanttExportInfo }); addSelectedCheck(checks, options, "frontend:pipeline-gantt-observation-live-running", Boolean(pipelineObservationGanttMetrics?.candidate) && Number(pipelineObservationGanttMetrics?.observationArrowCount || 0) > 0 && Number(pipelineObservationGanttMetrics?.observationSourceMarkerCount || 0) === 0 && (pipelineObservationGanttMetrics?.observationArrowTargetInsetsPx || []).some((value: number) => value >= 8) && Number(pipelineObservationGanttMetrics?.liveRunningBarCount || 0) > 0 && (pipelineObservationGanttMetrics?.liveRunningHeights || []).some((height: number) => height >= 24) && (pipelineObservationGanttMetrics?.runningAnimationNames || []).some((name: string) => String(name || "").includes("ganttPulse")) && pipelineObservationGanttMetrics?.hasLiveSweep === true, { pipelineObservationGanttMetrics }); addSelectedCheck(checks, options, "frontend:pipeline-step-timeline-visible", pipelineStepTimelineText.includes("OpenCode Trace") && pipelineStepTimelineText.includes("Code Queue") && pipelineStepTimelineText.toLowerCase().includes("tools") && pipelineStepTimelineText.includes("Trace") && !firstPipelineStepSummaryText.includes("{\n") && pipelineSessionHeadText.includes("agent") && pipelineSessionHeadText.toLowerCase().includes("model") && !firstPipelineStepSummaryText.toLowerCase().includes("tokens") && !firstPipelineStepSummaryText.includes("\nbuild\n") && !pipelineTimelineMetrics.hasHorizontalScroll && !pipelineTimelineMetrics.flowConnectorVisible && Number(pipelineTimelineMetrics.oldPipelineStepStyleCount || 0) === 0 && Number(pipelineTimelineMetrics.emptyAttemptDetailCount || 0) === 0 && !firstPipelineStepSummaryMetrics.hasHorizontalScroll && firstPipelineStepSummaryMetrics.clientHeight <= 900 && (firstPipelineStepExpandedText.includes("Message") || firstPipelineStepExpandedText.includes("Tool calls") || firstPipelineStepExpandedText.includes("Ran")), { pipelineStepTimelinePreview: pipelineStepTimelineText.slice(0, 1600), pipelineSessionHeadText, pipelineTimelineMetrics, firstPipelineStepSummaryMetrics, firstPipelineStepSummaryPreview: firstPipelineStepSummaryText.slice(0, 800), firstPipelineStepExpandedPreview: firstPipelineStepExpandedText.slice(0, 1000), }); addSelectedCheck(checks, options, "frontend:met-nonlinear-integrated-visible", metNonlinearTextLower.includes("met nonlinear 训练编排") && metNonlinearInitialText.includes("D601") && metNonlinearInitialText.includes("当前队列") && metNonlinearInitialText.includes("GPU/镜像") && metNonlinearInitialText.includes("Fork Project") && metNonlinearInitialText.includes("加入待启动队列") && metNonlinearInitialText.includes("启动队列") && !metNonlinearInitialText.includes("创建10个10轮任务") && metNonlinearInitialText.includes("仅 UniDesk frontend 代理访问") && /Health\s+OK/i.test(metNonlinearInitialText), { metNonlinearTextPreview: metNonlinearInitialText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:met-nonlinear-project-tree-detail", metProjectTreeText.includes("projects") && metProjectTreeText.includes("ex_projects") && metProjectDetailText.toLowerCase().includes("project 详情") && metProjectDetailText.toLowerCase().includes("config.json") && metProjectDetailText.toLowerCase().includes("data/ 训练状态") && metProjectDetailText.includes("模型参数") && metProjectDetailText.includes("指标") && metProjectDetailText.toLowerCase().includes("total params") && !metProjectDetailText.includes('{\n'), { metProjectTreePreview: metProjectTreeText.slice(0, 1200), metProjectDetailPreview: metProjectDetailText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:met-nonlinear-queue-detail-speed", metCompletedText.includes("速度") && metCompletedText.toLowerCase().includes("epoch/h") && metJobDetailText.includes("训练任务详情") && metJobDetailText.includes("训练速度") && metJobDetailText.toLowerCase().includes("epoch/h") && metJobDetailText.toLowerCase().includes("config.json") && metJobDetailText.toLowerCase().includes("data/ 训练状态"), { metCompletedPreview: metCompletedText.slice(0, 1200), metJobDetailPreview: metJobDetailText.slice(0, 1400) }); addSelectedCheck(checks, options, "frontend:layout-overflow-desktop", layoutOverflowDesktop.length > 0 && layoutOverflowDesktop.every((item) => item.ok), { probes: layoutOverflowDesktop }); addSelectedCheck(checks, options, "frontend:layout-overflow-mobile", layoutOverflowMobile.length > 0 && layoutOverflowMobile.every((item) => item.ok), { probes: layoutOverflowMobile }); addSelectedCheck(checks, options, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors }); return { screenshotPath, bodyText, consoleErrors }; } finally { await browser.close(); } } export async function runE2E( config: UniDeskConfig, options: E2ERunOptions = { only: [], skip: [] }, ): Promise { const selectedChecks = ALL_E2E_CHECK_NAMES.filter((name) => wantsCheck(options, name)); if (selectedChecks.length === 0) { throw new Error("e2e run selection matched no checks"); } const checks: E2ECheck[] = []; const urls = publicUrls(config); const needNetwork = wantsPrefix(options, "network"); const needService = wantsPrefix(options, "core") || wantsPrefix(options, "provider") || wantsPrefix(options, "provider-ingress") || wantsPrefix(options, "microservice"); const needDatabase = wantsPrefix(options, "database") || wantsCheck(options, "frontend:task-history-diagnostics"); const onlyRetryTraceFixture = options.only.length > 0 && selectedChecks.length > 0 && selectedChecks.every((name) => (CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES as readonly string[]).includes(name)); const onlyCodeQueueBrowserFixtures = options.only.length > 0 && selectedChecks.length > 0 && selectedChecks.every((name) => [ ...(CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES as readonly string[]), ...(CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES as readonly string[]), ...(CODE_QUEUE_LIVENESS_UI_FIXTURE_CHECK_NAMES as readonly string[]), ].includes(name)); const needFrontend = wantsPrefix(options, "frontend") && !onlyRetryTraceFixture && !onlyCodeQueueBrowserFixtures; const needRetryTraceFixture = wantsAnyCheck(options, [...CODE_QUEUE_RETRY_TRACE_FIXTURE_CHECK_NAMES]); const needStatsFixture = wantsAnyCheck(options, [...CODE_QUEUE_STATS_FIXTURE_CHECK_NAMES]); const needLivenessUiFixture = wantsAnyCheck(options, [...CODE_QUEUE_LIVENESS_UI_FIXTURE_CHECK_NAMES]); const needCodeQueueFixtures = wantsAnyCheck(options, [...CODE_QUEUE_FIXTURE_CHECK_NAMES]); const executedSections: string[] = []; if (needCodeQueueFixtures) { executedSections.push("code-queue-fixtures"); codeQueueFixtureChecks(checks, options); } if (needNetwork) { executedSections.push("network"); await exposureChecks(config, urls, checks, options); } if (needService) { executedSections.push("service"); await serviceChecks(config, urls, checks, options); } let markerId: string | null = null; if (needDatabase) { executedSections.push("database"); markerId = databaseChecks(config, checks, options); } let frontend: { screenshotPath: string; bodyText: string; consoleErrors: string[] } | null = null; if (needFrontend) { executedSections.push("frontend"); frontend = await frontendCheck(config, urls, checks, options); } else if (needRetryTraceFixture || needStatsFixture || needLivenessUiFixture) { executedSections.push("frontend-code-queue-browser-fixtures"); const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 1440, height: 920 } }); const auth = { username: process.env.UNIDESK_E2E_AUTH_USERNAME || config.auth.username, password: process.env.UNIDESK_E2E_AUTH_PASSWORD || config.auth.password, }; await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 }); await page.waitForSelector('[data-testid="login-screen"]', { timeout: 10000 }); await page.fill('input[name="username"]', auth.username); await page.fill('input[name="password"]', auth.password); await page.click('button[type="submit"]'); await page.waitForSelector('[data-testid="app-shell"]', { timeout: 10000 }); const codeQueueRetryTraceFixtureMetrics = needRetryTraceFixture ? await runCodeQueueRetryTraceFixture(page, urls.frontendUrl) : { checked: false }; const codeQueueStatsVisibleMetrics = wantsCheck(options, "frontend:code-queue-stats-visible") ? await runCodeQueueStatsFixture(page, urls.frontendUrl, "visible") : { checked: false }; const codeQueueStatsDegradedMetrics = wantsCheck(options, "frontend:code-queue-stats-degraded-visible") ? await runCodeQueueStatsFixture(page, urls.frontendUrl, "degraded") : { checked: false }; const codeQueueSplitBrainLiveMetrics = wantsCheck(options, "frontend:code-queue-split-brain-live-not-failed") ? await runCodeQueueSplitBrainFixture(page, urls.frontendUrl, "live") : { checked: false }; const codeQueueSplitBrainStaleMetrics = wantsCheck(options, "frontend:code-queue-split-brain-stale-failed") ? await runCodeQueueSplitBrainFixture(page, urls.frontendUrl, "stale") : { checked: false }; addSelectedCheck(checks, options, "frontend:code-queue-stats-visible", codeQueueStatsVisibleMetrics.checked === true && codeQueueStatsVisibleMetrics.ok === true, { codeQueueStatsVisibleMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-stats-degraded-visible", codeQueueStatsDegradedMetrics.checked === true && codeQueueStatsDegradedMetrics.ok === true, { codeQueueStatsDegradedMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-split-brain-live-not-failed", codeQueueSplitBrainLiveMetrics.checked === true && codeQueueSplitBrainLiveMetrics.ok === true && codeQueueSplitBrainLiveMetrics.healthFailed === false && codeQueueSplitBrainLiveMetrics.health?.value === "split-brain live" && codeQueueSplitBrainLiveMetrics.health?.className.includes("degraded-live") && codeQueueSplitBrainLiveMetrics.effective?.value === "live" && codeQueueSplitBrainLiveMetrics.heartbeatRisk?.value === "0" && Number(codeQueueSplitBrainLiveMetrics.failedMetricCount || 0) === 0 && String(codeQueueSplitBrainLiveMetrics.panelText || "").includes("continue-supervision") && String(codeQueueSplitBrainLiveMetrics.panelText || "").includes("控制面/执行面观测分裂") && !/任务死亡|dead task|task death|必须重启|must restart/i.test(String(codeQueueSplitBrainLiveMetrics.panelText || "")), { codeQueueSplitBrainLiveMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-split-brain-stale-failed", codeQueueSplitBrainStaleMetrics.checked === true && codeQueueSplitBrainStaleMetrics.ok === true && codeQueueSplitBrainStaleMetrics.healthFailed === true && codeQueueSplitBrainStaleMetrics.effective?.value === "at-risk" && codeQueueSplitBrainStaleMetrics.heartbeatRisk?.value === "1" && codeQueueSplitBrainStaleMetrics.heartbeatRisk?.className.includes("failed") && codeQueueSplitBrainStaleMetrics.staleCandidates?.value === "1" && String(codeQueueSplitBrainStaleMetrics.panelText || "").includes("investigate-heartbeat-risk"), { codeQueueSplitBrainStaleMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-retry-attempt-trace-current", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.ok === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardAttemptVisible === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentAttempt === "2" && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentRunning === "true" && Number(codeQueueRetryTraceFixtureMetrics.currentStepRows || 0) >= 2 && (codeQueueRetryTraceFixtureMetrics.traceRequests || []).some((search: string) => String(search).includes("attempt=2")) && String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentText || "").includes("执行过程摘要 #2") && String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentText || "").includes("STEP 2 raw"), { codeQueueRetryTraceFixtureMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-step-missing-diagnostic", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepText !== "STEP --" && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepText !== "STEP --" && ["syncing", "fallback", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.cardStepState || "")) && ["syncing", "fallback", "raw-trace"].includes(String(codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentStepState || "")), { codeQueueRetryTraceFixtureMetrics }); addSelectedCheck(checks, options, "frontend:code-queue-judge-feedback-attempt-order", codeQueueRetryTraceFixtureMetrics.checked === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentBeforeJudge === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.judgeBeforeFeedback === true && codeQueueRetryTraceFixtureMetrics.beforeExpand?.currentJudgeMissing === true && String(codeQueueRetryTraceFixtureMetrics.feedbackText || "").includes("Continue with attempt 2") && (codeQueueRetryTraceFixtureMetrics.promptRequests || []).some((search: string) => String(search).includes("part=feedback") && String(search).includes("attempt=1")), { codeQueueRetryTraceFixtureMetrics }); } finally { await browser.close(); } } const ok = checks.every((check) => check.status === "passed"); const resultId = markerId ?? `e2e_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; const fullResult = { ok, urls, selection: { only: options.only, skip: options.skip, matchedChecks: selectedChecks, executedSections, }, markerId, screenshotPath: frontend?.screenshotPath ?? null, checks, }; mkdirSync(rootPath(".state", "e2e"), { recursive: true }); const resultPath = rootPath(".state", "e2e", `${resultId}_result.json`); writeFileSync(resultPath, `${JSON.stringify(fullResult, null, 2)}\n`, "utf8"); return { ok, urls, selection: { only: options.only, skip: options.skip, matchedChecks: selectedChecks, executedSections, }, markerId, screenshotPath: frontend?.screenshotPath ?? null, resultPath, checkCounts: { total: checks.length, passed: checks.filter((check) => check.status === "passed").length, failed: checks.filter((check) => check.status === "failed").length, }, checks: checks.map((check) => ({ name: check.name, status: check.status })), failedChecks: checks.filter((check) => check.status === "failed"), }; }