fix(dev): prevent dev frontend auth cache bleed
This commit is contained in:
+243
-93
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user