feat: add resource monitoring and provider upgrade
This commit is contained in:
+3
-3
@@ -23,8 +23,8 @@ function help(): unknown {
|
||||
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
|
||||
{ command: "job list", description: "List async jobs from .state/jobs." },
|
||||
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." },
|
||||
{ command: "debug health", description: "Probe internal core, overview, nodes, frontend, provider ingress, and public boundary." },
|
||||
{ command: "debug dispatch [providerId] [docker.ps|echo]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
|
||||
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|echo]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||
{ command: "e2e run", description: "Run public frontend/provider, internal core/database, and Playwright login E2E checks." },
|
||||
],
|
||||
};
|
||||
@@ -110,7 +110,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (sub === "dispatch") {
|
||||
const providerId = third ?? config.providerGateway.id;
|
||||
const dispatchCommand = fourth === "docker.ps" || fourth === "echo" ? fourth : "docker.ps";
|
||||
const dispatchCommand = fourth === "docker.ps" || fourth === "provider.upgrade" || fourth === "echo" ? fourth : "docker.ps";
|
||||
emitJson(commandName, await debugDispatch(config, providerId, dispatchCommand));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,17 @@ export interface UniDeskConfig {
|
||||
heartbeatIntervalMs: number;
|
||||
reconnectBaseMs: number;
|
||||
reconnectMaxMs: number;
|
||||
metrics: { diskPath: string };
|
||||
upgrade: {
|
||||
enabled: boolean;
|
||||
hostProjectRoot: string;
|
||||
workspacePath: string;
|
||||
composeFile: string;
|
||||
composeEnvFile: string;
|
||||
composeProject: string;
|
||||
service: string;
|
||||
runnerImage: string;
|
||||
};
|
||||
};
|
||||
docker: { composeFile: string; projectName: string };
|
||||
paths: { stateDir: string; logsDir: string; docsReferenceDir: string };
|
||||
@@ -55,6 +66,12 @@ function numberField(obj: Record<string, unknown>, key: string, path: string): n
|
||||
return value;
|
||||
}
|
||||
|
||||
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function portPair(obj: Record<string, unknown>, key: string): { port: number; containerPort: number } {
|
||||
const value = asRecord(obj[key], `network.${key}`);
|
||||
return { port: numberField(value, "port", `network.${key}`), containerPort: numberField(value, "containerPort", `network.${key}`) };
|
||||
@@ -75,6 +92,8 @@ export function readConfig(): UniDeskConfig {
|
||||
const paths = asRecord(parsed.paths, "paths");
|
||||
const sshForwarding = asRecord(parsed.sshForwarding, "sshForwarding");
|
||||
const labels = asRecord(providerGateway.labels, "providerGateway.labels");
|
||||
const providerMetrics = asRecord(providerGateway.metrics, "providerGateway.metrics");
|
||||
const providerUpgrade = asRecord(providerGateway.upgrade, "providerGateway.upgrade");
|
||||
const typescript = stringField(runtime, "typescript", "runtime");
|
||||
if (typescript !== "bun") throw new Error("runtime.typescript must be bun");
|
||||
return {
|
||||
@@ -103,6 +122,19 @@ export function readConfig(): UniDeskConfig {
|
||||
heartbeatIntervalMs: numberField(providerGateway, "heartbeatIntervalMs", "providerGateway"),
|
||||
reconnectBaseMs: numberField(providerGateway, "reconnectBaseMs", "providerGateway"),
|
||||
reconnectMaxMs: numberField(providerGateway, "reconnectMaxMs", "providerGateway"),
|
||||
metrics: {
|
||||
diskPath: stringField(providerMetrics, "diskPath", "providerGateway.metrics"),
|
||||
},
|
||||
upgrade: {
|
||||
enabled: booleanField(providerUpgrade, "enabled", "providerGateway.upgrade"),
|
||||
hostProjectRoot: stringField(providerUpgrade, "hostProjectRoot", "providerGateway.upgrade"),
|
||||
workspacePath: stringField(providerUpgrade, "workspacePath", "providerGateway.upgrade"),
|
||||
composeFile: stringField(providerUpgrade, "composeFile", "providerGateway.upgrade"),
|
||||
composeEnvFile: stringField(providerUpgrade, "composeEnvFile", "providerGateway.upgrade"),
|
||||
composeProject: stringField(providerUpgrade, "composeProject", "providerGateway.upgrade"),
|
||||
service: stringField(providerUpgrade, "service", "providerGateway.upgrade"),
|
||||
runnerImage: stringField(providerUpgrade, "runnerImage", "providerGateway.upgrade"),
|
||||
},
|
||||
},
|
||||
docker: { composeFile: stringField(docker, "composeFile", "docker"), projectName: stringField(docker, "projectName", "docker") },
|
||||
paths: {
|
||||
|
||||
+84
-2
@@ -38,11 +38,93 @@ function coreInternalFetch(path: string, init?: { method?: string; body?: unknow
|
||||
}
|
||||
}
|
||||
|
||||
function coreDockerStatusSummary(): unknown {
|
||||
const code = `
|
||||
const res = await fetch('http://127.0.0.1:8080/api/nodes/docker-status');
|
||||
const text = await res.text();
|
||||
let body = null;
|
||||
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
|
||||
const dockerStatuses = (body?.dockerStatuses || []).map((item) => {
|
||||
const status = item.dockerStatus || {};
|
||||
return {
|
||||
providerId: item.providerId,
|
||||
name: item.name,
|
||||
nodeStatus: item.nodeStatus,
|
||||
updatedAt: item.updatedAt,
|
||||
dockerStatus: {
|
||||
ok: status.ok,
|
||||
socketPresent: status.socketPresent,
|
||||
collectedAt: status.collectedAt,
|
||||
counts: status.counts,
|
||||
daemon: status.daemon,
|
||||
containersPreview: (status.containers || []).slice(0, 8).map((container) => ({
|
||||
id: container.id,
|
||||
name: container.name,
|
||||
image: container.image,
|
||||
state: container.state,
|
||||
status: container.status,
|
||||
ports: container.ports,
|
||||
})),
|
||||
},
|
||||
};
|
||||
});
|
||||
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, dockerStatuses } }));
|
||||
`;
|
||||
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
|
||||
if (result.exitCode !== 0) {
|
||||
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout.trim()) as unknown;
|
||||
} catch {
|
||||
return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
|
||||
}
|
||||
}
|
||||
|
||||
function coreSystemStatusSummary(): unknown {
|
||||
const code = `
|
||||
const res = await fetch('http://127.0.0.1:8080/api/nodes/system-status?limit=24');
|
||||
const text = await res.text();
|
||||
let body = null;
|
||||
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
|
||||
const systemStatuses = (body?.systemStatuses || []).map((item) => {
|
||||
const current = item.current || {};
|
||||
return {
|
||||
providerId: item.providerId,
|
||||
name: item.name,
|
||||
nodeStatus: item.nodeStatus,
|
||||
updatedAt: item.updatedAt,
|
||||
current: item.current ? {
|
||||
ok: current.ok,
|
||||
collectedAt: current.collectedAt,
|
||||
cpu: current.cpu,
|
||||
memory: current.memory,
|
||||
disk: current.disk,
|
||||
} : null,
|
||||
historyPreview: (item.history || []).slice(-8),
|
||||
historyCount: (item.history || []).length,
|
||||
};
|
||||
});
|
||||
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, systemStatuses } }));
|
||||
`;
|
||||
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
|
||||
if (result.exitCode !== 0) {
|
||||
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(result.stdout.trim()) as unknown;
|
||||
} catch {
|
||||
return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function debugHealth(config: UniDeskConfig): Promise<unknown> {
|
||||
return {
|
||||
coreInternal: await coreInternalFetch("/health"),
|
||||
overviewInternal: await coreInternalFetch("/api/overview"),
|
||||
nodesInternal: await coreInternalFetch("/api/nodes"),
|
||||
systemStatusInternal: coreSystemStatusSummary(),
|
||||
dockerStatusInternal: coreDockerStatusSummary(),
|
||||
frontendPublic: await readJson(`http://127.0.0.1:${config.network.frontend.port}/health`),
|
||||
providerIngressPublic: await readJson(`http://127.0.0.1:${config.network.providerIngress.port}/health`),
|
||||
publicExposureBoundary: {
|
||||
@@ -52,9 +134,9 @@ export async function debugHealth(config: UniDeskConfig): Promise<unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function debugDispatch(config: UniDeskConfig, providerId: string, command: "docker.ps" | "echo"): Promise<unknown> {
|
||||
export async function debugDispatch(config: UniDeskConfig, providerId: string, command: "docker.ps" | "provider.upgrade" | "echo"): Promise<unknown> {
|
||||
return coreInternalFetch("/api/dispatch", {
|
||||
method: "POST",
|
||||
body: { providerId, command, payload: { source: "cli-debug" } },
|
||||
body: { providerId, command, payload: command === "provider.upgrade" ? { source: "cli-debug", mode: "plan" } : { source: "cli-debug" } },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,6 +83,15 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_HEARTBEAT_TIMEOUT_MS: "90000",
|
||||
UNIDESK_RECONNECT_BASE_MS: String(config.providerGateway.reconnectBaseMs),
|
||||
UNIDESK_RECONNECT_MAX_MS: String(config.providerGateway.reconnectMaxMs),
|
||||
UNIDESK_MONITOR_DISK_PATH: config.providerGateway.metrics.diskPath,
|
||||
UNIDESK_PROVIDER_UPGRADE_ENABLED: String(config.providerGateway.upgrade.enabled),
|
||||
UNIDESK_PROVIDER_UPGRADE_HOST_PROJECT_ROOT: config.providerGateway.upgrade.hostProjectRoot,
|
||||
UNIDESK_PROVIDER_UPGRADE_WORKSPACE_PATH: config.providerGateway.upgrade.workspacePath,
|
||||
UNIDESK_PROVIDER_UPGRADE_COMPOSE_FILE: config.providerGateway.upgrade.composeFile,
|
||||
UNIDESK_PROVIDER_UPGRADE_ENV_FILE: config.providerGateway.upgrade.composeEnvFile,
|
||||
UNIDESK_PROVIDER_UPGRADE_COMPOSE_PROJECT: config.providerGateway.upgrade.composeProject,
|
||||
UNIDESK_PROVIDER_UPGRADE_SERVICE: config.providerGateway.upgrade.service,
|
||||
UNIDESK_PROVIDER_UPGRADE_RUNNER_IMAGE: config.providerGateway.upgrade.runnerImage,
|
||||
UNIDESK_LOG_DIR: logDir,
|
||||
UNIDESK_LOG_PREFIX: logPrefix,
|
||||
UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
|
||||
|
||||
+110
-2
@@ -71,9 +71,13 @@ function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: str
|
||||
return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode };
|
||||
}
|
||||
|
||||
function dockerCoreJson(path: string): unknown {
|
||||
function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown {
|
||||
const code = `
|
||||
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)});
|
||||
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
|
||||
method: init?.method ?? "GET",
|
||||
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
|
||||
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
|
||||
})});
|
||||
const text = await res.text();
|
||||
let body = null;
|
||||
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
|
||||
@@ -88,6 +92,20 @@ function dockerCoreJson(path: string): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForTaskStatus(taskId: string, expected: string, timeoutMs = 10_000): Promise<unknown> {
|
||||
const started = Date.now();
|
||||
let latest: unknown = null;
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
latest = dockerCoreJson("/api/tasks?limit=40");
|
||||
const tasks = (latest as { body?: { tasks?: Array<{ id?: string; status?: string; result?: unknown }> } }).body?.tasks ?? [];
|
||||
const task = tasks.find((item) => item.id === taskId);
|
||||
if (task?.status === expected) return { ok: true, task };
|
||||
if (task?.status === "failed") return { ok: false, task };
|
||||
await Bun.sleep(500);
|
||||
}
|
||||
return { ok: false, timeoutMs, latest };
|
||||
}
|
||||
|
||||
function dockerPortSummary(): unknown {
|
||||
const result = runCommand([
|
||||
"docker",
|
||||
@@ -108,6 +126,54 @@ function dockerPortSummary(): unknown {
|
||||
};
|
||||
}
|
||||
|
||||
function dockerStatusCheckDetail(dockerStatus: unknown, providerId: string): unknown {
|
||||
const response = dockerStatus as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; dockerStatus?: { ok?: boolean; socketPresent?: boolean; collectedAt?: string; counts?: unknown; daemon?: unknown; containers?: Array<{ id?: string; name?: string; image?: string; state?: string; status?: string; ports?: string }> } }> } };
|
||||
const item = response.body?.dockerStatuses?.find((entry) => entry.providerId === providerId);
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
providerId,
|
||||
nodeStatus: item?.nodeStatus,
|
||||
updatedAt: item?.updatedAt,
|
||||
dockerStatus: item?.dockerStatus === undefined ? null : {
|
||||
ok: item.dockerStatus.ok,
|
||||
socketPresent: item.dockerStatus.socketPresent,
|
||||
collectedAt: item.dockerStatus.collectedAt,
|
||||
counts: item.dockerStatus.counts,
|
||||
daemon: item.dockerStatus.daemon,
|
||||
containersPreview: (item.dockerStatus.containers ?? []).slice(0, 8).map((container) => ({
|
||||
id: container.id,
|
||||
name: container.name,
|
||||
image: container.image,
|
||||
state: container.state,
|
||||
status: container.status,
|
||||
ports: container.ports,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function systemStatusCheckDetail(systemStatus: unknown, providerId: string): unknown {
|
||||
const response = systemStatus as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<{ providerId?: string; name?: string; nodeStatus?: string; updatedAt?: string; current?: { ok?: boolean; collectedAt?: string; cpu?: unknown; memory?: unknown; disk?: unknown }; history?: unknown[] }> } };
|
||||
const item = response.body?.systemStatuses?.find((entry) => entry.providerId === providerId);
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
providerId,
|
||||
nodeStatus: item?.nodeStatus,
|
||||
updatedAt: item?.updatedAt,
|
||||
current: item?.current === undefined ? null : {
|
||||
ok: item.current.ok,
|
||||
collectedAt: item.current.collectedAt,
|
||||
cpu: item.current.cpu,
|
||||
memory: item.current.memory,
|
||||
disk: item.current.disk,
|
||||
},
|
||||
historyCount: item?.history?.length ?? 0,
|
||||
historyPreview: (item?.history ?? []).slice(-8),
|
||||
};
|
||||
}
|
||||
|
||||
async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
const portSummary = dockerPortSummary() as { rows?: Array<{ name: string; ports: string }> };
|
||||
const portsText = (portSummary.rows ?? []).map((row) => `${row.name} ${row.ports}`).join("\n");
|
||||
@@ -137,11 +203,27 @@ async function exposureChecks(config: UniDeskConfig, urls: PublicUrls, checks: E
|
||||
async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
const coreOverview = dockerCoreJson("/api/overview");
|
||||
const coreNodes = dockerCoreJson("/api/nodes");
|
||||
const systemStatus = dockerCoreJson("/api/nodes/system-status?limit=24");
|
||||
const dockerStatus = dockerCoreJson("/api/nodes/docker-status");
|
||||
const providerIngress = await fetchProbe(urls.providerIngressHealthUrl);
|
||||
const overviewBody = (coreOverview as { body?: { ok?: boolean; dbReady?: boolean; onlineNodeCount?: number } }).body;
|
||||
const nodeList = (coreNodes as { body?: { nodes?: Array<{ providerId?: string; status?: string }> } }).body?.nodes ?? [];
|
||||
const systemStatuses = (systemStatus as { body?: { systemStatuses?: Array<{ providerId?: string; current?: { cpu?: { percent?: number }; memory?: { percent?: number }; disk?: { percent?: number } }; history?: unknown[] }> } }).body?.systemStatuses ?? [];
|
||||
const mainSystem = systemStatuses.find((item) => item.providerId === config.providerGateway.id);
|
||||
const dockerStatuses = (dockerStatus as { body?: { dockerStatuses?: Array<{ providerId?: string; dockerStatus?: { counts?: { containers?: number }; containers?: unknown[] } }> } }).body?.dockerStatuses ?? [];
|
||||
const mainDocker = dockerStatuses.find((item) => item.providerId === config.providerGateway.id);
|
||||
addCheck(checks, "core:internal-overview", (coreOverview as { ok?: boolean }).ok === true && overviewBody?.ok === true && overviewBody.dbReady === true && (overviewBody.onlineNodeCount ?? 0) >= 1, coreOverview);
|
||||
addCheck(checks, "provider:self-node-online", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), coreNodes);
|
||||
addCheck(checks, "provider:system-status", (systemStatus as { ok?: boolean }).ok === true && mainSystem?.current !== undefined && Number.isFinite(mainSystem.current.cpu?.percent) && Number.isFinite(mainSystem.current.memory?.percent) && Number.isFinite(mainSystem.current.disk?.percent) && (mainSystem.history?.length ?? 0) > 0, systemStatusCheckDetail(systemStatus, config.providerGateway.id));
|
||||
addCheck(checks, "provider:docker-status", (dockerStatus as { ok?: boolean }).ok === true && mainDocker?.dockerStatus !== undefined && ((mainDocker.dockerStatus.counts?.containers ?? 0) > 0 || (mainDocker.dockerStatus.containers?.length ?? 0) > 0), dockerStatusCheckDetail(dockerStatus, config.providerGateway.id));
|
||||
const upgradeDispatch = dockerCoreJson("/api/dispatch", {
|
||||
method: "POST",
|
||||
body: { providerId: config.providerGateway.id, command: "provider.upgrade", payload: { source: "cli-e2e", mode: "plan" } },
|
||||
});
|
||||
const upgradeTaskId = (upgradeDispatch as { body?: { taskId?: string } }).body?.taskId ?? "";
|
||||
const upgradeTask = upgradeTaskId ? await waitForTaskStatus(upgradeTaskId, "succeeded") : { ok: false, error: "missing taskId", upgradeDispatch };
|
||||
const taskResult = (upgradeTask as { task?: { result?: { plan?: unknown; mode?: string } }; ok?: boolean }).task?.result;
|
||||
addCheck(checks, "provider:upgrade-plan", (upgradeDispatch as { ok?: boolean }).ok === true && (upgradeTask as { ok?: boolean }).ok === true && taskResult?.mode === "plan" && taskResult.plan !== undefined, { upgradeDispatch, upgradeTask });
|
||||
addCheck(checks, "provider-ingress:public-health", (providerIngress as { ok?: boolean; body?: { ok?: boolean } }).ok === true && (providerIngress as { body?: { ok?: boolean } }).body?.ok === true, providerIngress);
|
||||
}
|
||||
|
||||
@@ -192,9 +274,35 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
await page.getByTestId(`raw-node-${config.providerGateway.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`).click();
|
||||
await page.waitForSelector('[data-testid="raw-json"]', { timeout: 5000 });
|
||||
const rawText = await page.locator('[data-testid="raw-json"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: "关闭" }).click();
|
||||
await page.getByRole("button", { name: /资源节点/ }).click();
|
||||
await page.getByRole("button", { name: /资源监控/ }).click();
|
||||
await page.waitForSelector('[data-testid="node-monitor-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-cpu"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-memory"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="metric-chart-disk"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText.toLowerCase();
|
||||
return text.includes("任务管理器视图") && text.includes("cpu") && text.includes("memory") && text.includes("disk");
|
||||
}, undefined, { timeout: 10000 });
|
||||
const monitorText = await page.locator('[data-testid="node-monitor-page"]').innerText({ timeout: 5000 });
|
||||
await page.getByTestId("upgrade-plan-button").click();
|
||||
await page.waitForFunction(() => document.body.innerText.includes("预检升级 已下发"), undefined, { timeout: 10000 });
|
||||
const upgradeControlText = await page.locator('[data-testid="provider-upgrade-control"]').innerText({ timeout: 5000 });
|
||||
await page.getByRole("button", { name: /Docker 状态/ }).click();
|
||||
await page.waitForSelector('[data-testid="docker-status-page"]', { timeout: 10000 });
|
||||
await page.waitForSelector('[data-testid="docker-container-table"]', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const text = document.body.innerText.toLowerCase();
|
||||
return text.includes("docker desktop 视图") && text.includes("containers");
|
||||
}, undefined, { timeout: 10000 });
|
||||
const dockerText = await page.locator('[data-testid="docker-status-page"]').innerText({ timeout: 5000 });
|
||||
addCheck(checks, "frontend:login-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name) && bodyText.includes("核心在线"), { screenshotPath });
|
||||
addCheck(checks, "frontend:no-naked-json-before-click", rawBlocksBefore === 0 && !nakedJsonText, { rawBlocksBefore, nakedJsonText });
|
||||
addCheck(checks, "frontend:raw-json-explicit-button", rawText.includes('"providerId"') && rawText.includes(config.providerGateway.id), { rawTextPreview: rawText.slice(0, 400) });
|
||||
addCheck(checks, "frontend:system-monitor-visible", monitorText.includes("任务管理器视图") && monitorText.includes("CPU") && monitorText.includes("Memory") && monitorText.includes("Disk"), { monitorTextPreview: monitorText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:upgrade-plan-dispatch", upgradeControlText.includes("预检升级 已下发"), { providerId: config.providerGateway.id, upgradeControlPreview: upgradeControlText.slice(0, 500) });
|
||||
addCheck(checks, "frontend:docker-status-visible", dockerText.toLowerCase().includes("docker desktop 视图") && dockerText.toLowerCase().includes("containers") && (dockerText.includes("unidesk-frontend") || dockerText.includes("unidesk-backend-core")), { dockerTextPreview: dockerText.slice(0, 800) });
|
||||
addCheck(checks, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors });
|
||||
return { screenshotPath, bodyText, consoleErrors };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user