diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index 3323d71c..e5f7a799 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -25,6 +25,10 @@ interface PublicUrls { blockedDatabasePort: number; } +interface E2EApiClient { + getJson(path: string, init?: { method?: string; body?: unknown }): unknown; +} + export interface E2ERunOptions { only: string[]; skip: string[]; @@ -505,6 +509,10 @@ function publicUrls(config: UniDeskConfig): PublicUrls { }; } +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); @@ -538,6 +546,63 @@ function tcpProbe(host: string, port: number, timeoutMs = 2500): 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, @@ -953,6 +1018,67 @@ function dockerCoreJson(path: string, init?: { method?: string; body?: unknown } } } +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, "'\\''")}'`; } @@ -1212,69 +1338,72 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E } async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[], options: E2ERunOptions): Promise { - const coreOverview = dockerCoreJson("/api/overview"); - const corePerformance = dockerCoreJson("/api/performance"); - const coreNodes = dockerCoreJson("/api/nodes"); - const systemStatus = dockerCoreJson("/api/nodes/system-status?limit=24"); - const dockerStatus = dockerCoreJson("/api/nodes/docker-status"); - const microservices = dockerCoreJson("/api/microservices"); - const findjobStatus = dockerCoreJson("/api/microservices/findjob/status"); - const findjobHealth = dockerCoreJson("/api/microservices/findjob/health"); - const findjobSummary = dockerCoreJson("/api/microservices/findjob/proxy/api/summary"); - const findjobJobsPreview = dockerCoreJson("/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5"); - const pipelineStatus = dockerCoreJson("/api/microservices/pipeline/status"); - const pipelineHealth = dockerCoreJson("/api/microservices/pipeline/health"); - const pipelineSnapshot = dockerCoreJson("/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3"); - const pipelineOaEventFlow = dockerCoreJson("/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics"); - const metNonlinearStatus = dockerCoreJson("/api/microservices/met-nonlinear/status"); - const metNonlinearHealth = dockerCoreJson("/api/microservices/met-nonlinear/health"); - const metNonlinearQueue = dockerCoreJson("/api/microservices/met-nonlinear/proxy/api/queue?__unideskArrayLimit=jobs:10"); - const metNonlinearProjects = dockerCoreJson("/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=20&__unideskArrayLimit=projects:20"); - const metNonlinearImages = dockerCoreJson("/api/microservices/met-nonlinear/proxy/api/images"); - const claudeqqStatus = dockerCoreJson("/api/microservices/claudeqq/status"); - const claudeqqHealth = dockerCoreJson("/api/microservices/claudeqq/health"); - const claudeqqNapcatLogin = dockerCoreJson("/api/microservices/claudeqq/proxy/api/napcat/login"); - const claudeqqEvents = dockerCoreJson("/api/microservices/claudeqq/proxy/api/events/recent?limit=5"); - const claudeqqSubscriptions = dockerCoreJson("/api/microservices/claudeqq/proxy/api/events/subscriptions"); - const todoNoteStatus = dockerCoreJson("/api/microservices/todo-note/status"); - const todoNoteHealth = dockerCoreJson("/api/microservices/todo-note/health"); - const todoNoteInstances = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances"); - const oaEventFlowStatus = dockerCoreJson("/api/microservices/oa-event-flow/status"); - const oaEventFlowHealth = dockerCoreJson("/api/microservices/oa-event-flow/health"); - const oaEventFlowDiagnostics = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/diagnostics"); - const oaEventFlowEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?limit=10"); - const oaEventFlowPipelineEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline&limit=10"); - const oaEventFlowStats = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/stats/trace?limit=10"); - const k3sctlStatus = dockerCoreJson("/api/microservices/k3sctl-adapter/status"); - const k3sctlControlPlane = dockerCoreJson("/api/microservices/k3sctl-adapter/proxy/api/control-plane"); - const codeQueueStatus = dockerCoreJson("/api/microservices/code-queue/status"); - const codeQueueHealth = dockerCoreJson("/api/microservices/code-queue/health"); + 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") - ? dockerCoreJson("/api/microservices/code-queue/proxy/api/workdirs") + ? apiClient.getJson("/api/microservices/code-queue/proxy/api/workdirs") : null; - const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId="); - const decisionCenterStatus = dockerCoreJson("/api/microservices/decision-center/status"); - const decisionCenterHealth = dockerCoreJson("/api/microservices/decision-center/health"); - const decisionCenterRecords = dockerCoreJson("/api/microservices/decision-center/proxy/api/records?limit=20"); - const filebrowserHealth = dockerCoreJson("/api/microservices/filebrowser/health"); - const filebrowserWebui = dockerCoreJson("/api/microservices/filebrowser/proxy/"); - const filebrowserD601Health = dockerCoreJson("/api/microservices/filebrowser-d601/health"); + 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 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 = dockerCoreJson("/api/microservices/todo-note/proxy/api/instances", { method: "POST", body: { name: todoE2eName } }); + 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 - ? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "addTodo", title: "E2E migrated write path" } } }) + ? 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 - ? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/actions`, { method: "POST", body: { action: { type: "toggleTodoCompleted", todoId: 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 - ? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}/undo`, { method: "POST", body: {} }) + ? 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 - ? dockerCoreJson(`/api/microservices/todo-note/proxy/api/instances/${encodeURIComponent(todoCreatedId)}`, { method: "DELETE" }) + ? 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; @@ -1378,6 +1507,23 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 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 }); @@ -1516,14 +1662,14 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 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 && (codeQueueStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", codeQueueStatus); - addSelectedCheck(checks, options, "microservice:code-queue-health", (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", codeQueueHealth); + 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 === "/workspace") + && 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); @@ -1640,6 +1786,15 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 "frontend:code-queue-judge-wrap", "frontend:code-queue-error-red-markers", ]); + 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", + ]); const needClaudeqq = wants("frontend:claudeqq-integrated-visible"); const needRouteDeepLink = wants("frontend:url-route-deeplink"); const needPipeline = wantsAny([ @@ -1682,8 +1837,10 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 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); - await page.waitForSelector(`text=${config.providerGateway.id}`, { timeout: 10000 }); - await page.waitForSelector(`text=${config.providerGateway.name}`, { timeout: 10000 }); + 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[] = []; @@ -2195,26 +2352,28 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 queueValues: options.map((option) => (option as HTMLOptionElement).value).filter((value) => value !== "__all__"), switched: false, })); - 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 (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 => { @@ -2576,7 +2735,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 }; } } - codeQueueOutputText = await page.locator('[data-testid="codex-output"]').innerText({ timeout: 5000 }); + 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"); @@ -3127,19 +3286,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 }; }); await authContext.close(); - const unauthContext = await browser.newContext({ viewport: { width: 1440, height: 920 } }); - const unauthPage = await unauthContext.newPage(); - await unauthPage.goto(devFrontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 }); - const unauthenticatedDevResponse = await unauthPage.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, - }; - }); - await unauthContext.close(); + const unauthenticatedDevResponse = await rawHttpProbe(`${devFrontendUrl}/api/microservices`, 15_000); devAuthApiMetrics = { checked: true, devOrigin: devFrontendUrl, @@ -3217,14 +3364,17 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2 && !decisionCenterText.includes("{\n"), { decisionCenterMetrics, decisionCenterE2eRecord, decisionCenterDeleteResult, decisionCenterTextPreview: decisionCenterText.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.cwdValue === "/workspace" - && codeQueueSubmitQueueControl.workdirSelectValue === "/workspace" + codeQueueSubmitQueueControl.providerValue === expectedFrontendCodeQueueProvider + && codeQueueSubmitQueueControl.cwdValue === expectedFrontendCodeQueueWorkdir + && codeQueueSubmitQueueControl.workdirSelectValue === expectedFrontendCodeQueueWorkdir && Array.isArray(codeQueueSubmitQueueControl.workdirSelectOptionTexts) - && codeQueueSubmitQueueControl.workdirSelectOptionTexts.some((text: string) => text.includes("/workspace")) + && codeQueueSubmitQueueControl.workdirSelectOptionTexts.some((text: string) => text.includes(expectedFrontendCodeQueueWorkdir)) && !codeQueueText.includes("加载工作目录失败") && !codeQueueText.includes("HTTP 404"), - { codeQueueSubmitQueueControl, codeQueueTextPreview: codeQueueText.slice(0, 1200) }); + { expectedFrontendCodeQueueProvider, expectedFrontendCodeQueueWorkdir, codeQueueSubmitQueueControl, codeQueueTextPreview: codeQueueText.slice(0, 1200) }); addSelectedCheck(checks, options, "frontend:code-queue-enqueue-await-smoke", codeQueueEnqueueAwaitSmoke.checked === true && codeQueueEnqueueAwaitSmoke.delayedPostCount === 1 diff --git a/src/components/dev-frontend-proxy/nginx.conf b/src/components/dev-frontend-proxy/nginx.conf index fc8ec6d9..49778b98 100644 --- a/src/components/dev-frontend-proxy/nginx.conf +++ b/src/components/dev-frontend-proxy/nginx.conf @@ -29,6 +29,10 @@ server { } location / { + set $dev_proxy_request_uri "$request_uri?__unideskDevProxyNoCache=$msec"; + if ($args != "") { + set $dev_proxy_request_uri "$request_uri&__unideskDevProxyNoCache=$msec"; + } proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; @@ -40,6 +44,6 @@ server { proxy_set_header Connection "upgrade"; proxy_buffering off; proxy_read_timeout 300s; - proxy_pass http://backend-core:8080/api/microservices/k3sctl-adapter/proxy/api/services/frontend-dev/proxy$request_uri; + proxy_pass http://backend-core:8080/api/microservices/k3sctl-adapter/proxy/api/services/frontend-dev/proxy$dev_proxy_request_uri; } }