fix(dev): prevent dev frontend auth cache bleed

This commit is contained in:
Codex
2026-05-19 06:42:55 +00:00
parent a0fd161b33
commit 65e52a0fb1
2 changed files with 248 additions and 94 deletions
+243 -93
View File
@@ -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<unknown> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -538,6 +546,63 @@ function tcpProbe(host: string, port: number, timeoutMs = 2500): Promise<unknown
});
}
function rawHttpProbe(urlText: string, timeoutMs = 5000): Promise<unknown> {
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<void> {
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
+5 -1
View File
@@ -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;
}
}