feat: initialize unidesk platform
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
|
||||
type CheckStatus = "passed" | "failed";
|
||||
|
||||
interface E2ECheck {
|
||||
name: string;
|
||||
status: CheckStatus;
|
||||
detail: unknown;
|
||||
}
|
||||
|
||||
interface PublicUrls {
|
||||
frontendUrl: string;
|
||||
coreUrl: string;
|
||||
databaseHost: string;
|
||||
databasePort: number;
|
||||
}
|
||||
|
||||
function publicUrls(config: UniDeskConfig): PublicUrls {
|
||||
return {
|
||||
frontendUrl: `http://${config.network.publicHost}:${config.network.frontend.port}`,
|
||||
coreUrl: `http://${config.network.publicHost}:${config.network.core.port}`,
|
||||
databaseHost: config.network.publicHost,
|
||||
databasePort: config.network.database.port,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchJson(url: string): Promise<unknown> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
const text = await response.text();
|
||||
return { ok: response.ok, status: response.status, body: text.length > 0 ? JSON.parse(text) : null };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function addCheck(checks: E2ECheck[], name: string, passed: boolean, detail: unknown): void {
|
||||
checks.push({ name, status: passed ? "passed" : "failed", detail });
|
||||
}
|
||||
|
||||
function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: string; stderr: string; exitCode: number | null } {
|
||||
const result = runCommand([
|
||||
"docker",
|
||||
"exec",
|
||||
"unidesk-database",
|
||||
"psql",
|
||||
"-U",
|
||||
config.database.user,
|
||||
"-d",
|
||||
config.database.name,
|
||||
"-v",
|
||||
"ON_ERROR_STOP=1",
|
||||
"-tA",
|
||||
"-c",
|
||||
sql,
|
||||
], repoRoot);
|
||||
return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode };
|
||||
}
|
||||
|
||||
async function apiChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<void> {
|
||||
const overview = await fetchJson(`${urls.coreUrl}/api/overview`);
|
||||
const nodes = await fetchJson(`${urls.coreUrl}/api/nodes`);
|
||||
addCheck(checks, "core:public-overview", (overview as { ok?: boolean; body?: { ok?: boolean; onlineNodeCount?: number } }).ok === true && (overview as { body?: { ok?: boolean; onlineNodeCount?: number } }).body?.ok === true && ((overview as { body?: { onlineNodeCount?: number } }).body?.onlineNodeCount ?? 0) >= 1, overview);
|
||||
const nodeList = (nodes as { body?: { nodes?: Array<{ providerId?: string; status?: string }> } }).body?.nodes ?? [];
|
||||
addCheck(checks, "core:public-nodes", nodeList.some((node) => node.providerId === config.providerGateway.id && node.status === "online"), nodes);
|
||||
}
|
||||
|
||||
function databaseChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): string {
|
||||
const markerId = `e2e_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
||||
const markerSql = `
|
||||
CREATE TABLE IF NOT EXISTS unidesk_e2e_markers (
|
||||
id TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
INSERT INTO unidesk_e2e_markers (id, source) VALUES ('${markerId}', 'cli-e2e');
|
||||
SELECT 'marker=' || id FROM unidesk_e2e_markers WHERE id = '${markerId}';
|
||||
SELECT 'marker_count=' || count(*) FROM unidesk_e2e_markers;
|
||||
SELECT 'online_main_server=' || count(*) FROM unidesk_nodes WHERE provider_id = '${config.providerGateway.id}' AND status = 'online';
|
||||
`;
|
||||
const marker = runPsql(config, markerSql);
|
||||
addCheck(checks, "database:named-volume-write", marker.ok && marker.stdout.includes(`marker=${markerId}`), marker);
|
||||
addCheck(checks, "database:provider-state", marker.ok && marker.stdout.includes("online_main_server=1"), marker);
|
||||
|
||||
const publicProbe = runCommand([
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"postgres:16-alpine",
|
||||
"pg_isready",
|
||||
"-h",
|
||||
urls.databaseHost,
|
||||
"-p",
|
||||
String(urls.databasePort),
|
||||
"-U",
|
||||
config.database.user,
|
||||
], repoRoot);
|
||||
addCheck(checks, "database:public-port", publicProbe.exitCode === 0, {
|
||||
exitCode: publicProbe.exitCode,
|
||||
stdout: publicProbe.stdout.trim(),
|
||||
stderr: publicProbe.stderr.trim(),
|
||||
});
|
||||
return markerId;
|
||||
}
|
||||
|
||||
async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2ECheck[]): Promise<{ screenshotPath: string; bodyText: string; consoleErrors: string[] }> {
|
||||
const e2eDir = rootPath(".state", "e2e");
|
||||
mkdirSync(e2eDir, { recursive: true });
|
||||
const screenshotPath = join(e2eDir, `${new Date().toISOString().replace(/[-:.TZ]/g, "")}_frontend.png`);
|
||||
const consoleErrors: string[] = [];
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 920 } });
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") consoleErrors.push(message.text());
|
||||
});
|
||||
page.on("pageerror", (error) => consoleErrors.push(error.message));
|
||||
await page.goto(urls.frontendUrl, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForFunction(() => document.querySelector("#conn-text")?.textContent?.includes("核心在线"), undefined, { timeout: 15000 });
|
||||
await page.waitForSelector(`text=${config.providerGateway.id}`, { timeout: 10000 });
|
||||
await page.waitForSelector("text=Online Nodes", { timeout: 5000 });
|
||||
const bodyText = await page.locator("body").innerText({ timeout: 5000 });
|
||||
const nodeCount = await page.locator("#node-count").innerText({ timeout: 5000 });
|
||||
const metricText = await page.locator("#metric-grid").innerText({ timeout: 5000 });
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
addCheck(checks, "frontend:public-page-provider-visible", bodyText.includes(config.providerGateway.id) && bodyText.includes(config.providerGateway.name), { nodeCount, metricText, screenshotPath });
|
||||
addCheck(checks, "frontend:no-console-errors", consoleErrors.length === 0, { consoleErrors });
|
||||
return { screenshotPath, bodyText, consoleErrors };
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runE2E(config: UniDeskConfig): Promise<unknown> {
|
||||
const checks: E2ECheck[] = [];
|
||||
const urls = publicUrls(config);
|
||||
await apiChecks(config, urls, checks);
|
||||
const markerId = databaseChecks(config, urls, checks);
|
||||
const frontend = await frontendCheck(config, urls, checks);
|
||||
const ok = checks.every((check) => check.status === "passed");
|
||||
return {
|
||||
ok,
|
||||
urls,
|
||||
markerId,
|
||||
screenshotPath: frontend.screenshotPath,
|
||||
checks,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user