Files
wehub-resource-sync 6d978fe483
Build / Frontend Build (push) Has been cancelled
Build / Backend Build (push) Has been cancelled
Build / Docker Build (push) Has been cancelled
Build / Kubernetes Deploy Smoke Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:24 +08:00

182 lines
5.6 KiB
TypeScript

import path from "node:path";
import { pathToFileURL } from "node:url";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { env, e2eRoot, repoRoot } from "../fixtures/env.js";
import { waitForHttpStatus, waitForTcpPort } from "./wait-for.js";
import {
createLogger,
DEBUG_SLOW_MO_MS,
defaultHeadlessFromEnv,
parseDebugArgs,
parseHeadlessArgs,
runCommand,
startService,
stopService,
suiteArgs,
type Suite,
writeFailureScreenshotSummary
} from "./runner-utils.js";
const npmCmd = "npm";
const npxCmd = "npx";
const goCmd = "go";
function childEnv(extra: NodeJS.ProcessEnv = {}) {
return {
...process.env,
E2E_FRONTEND_URL: env.frontendUrl,
E2E_BACKEND_URL: env.backendUrl,
E2E_DB_HOST: env.db.host,
E2E_DB_PORT: String(env.db.port),
E2E_DB_USER: env.db.user,
E2E_DB_PASSWORD: env.db.password,
E2E_DB_NAME: env.db.database,
E2E_RUNTIME_AGENT_TOKEN: env.runtimeAgentToken,
E2E_WORKSPACE_ROOT: env.workspaceRoot,
E2E_OBJECT_STORAGE_FALLBACK: env.objectStorageFallback,
...extra
};
}
export function parseArgs(
args: string[] = process.argv.slice(2),
environment: NodeJS.ProcessEnv = process.env
): { suite: Suite; ci: boolean; headless: boolean; debug: boolean; slowMoMs: number; extraArgs: string[] } {
const [suiteArg = "p0", ...rest] = args;
if (!["p0", "p1", "p2", "all"].includes(suiteArg)) {
throw new Error(`unknown e2e suite: ${suiteArg}`);
}
const ci = rest.includes("--ci");
const filteredArgs = rest.filter((value) => value !== "--ci" && value !== "--");
const parsedHeadless = parseHeadlessArgs(
filteredArgs,
ci || defaultHeadlessFromEnv(environment)
);
const parsedDebug = parseDebugArgs(parsedHeadless.args, false);
return {
suite: suiteArg as Suite,
ci,
headless: parsedHeadless.headless,
debug: parsedDebug.debug,
slowMoMs: parsedDebug.debug ? DEBUG_SLOW_MO_MS : 0,
extraArgs: parsedDebug.args
};
}
function frontendHostAndPort() {
const url = new URL(env.frontendUrl);
return {
host: url.hostname,
port: url.port || (url.protocol === "https:" ? "443" : "80")
};
}
async function main() {
const { suite, ci, headless, debug, slowMoMs, extraArgs } = parseArgs();
const logger = createLogger();
let backend: ChildProcessWithoutNullStreams | undefined;
let frontend: ChildProcessWithoutNullStreams | undefined;
try {
logger.log(`starting e2e suite=${suite} ci=${ci} headless=${headless} debug=${debug} slowMoMs=${slowMoMs}`);
logger.log(`run log: ${logger.logPath}`);
logger.log("starting MySQL");
await runCommand(
"docker",
["compose", "-f", "docker/docker-compose.e2e.yml", "up", "-d", "mysql"],
{ cwd: e2eRoot, label: "docker" },
logger.write
);
await waitForTcpPort(env.db.host, env.db.port, 120_000);
logger.log("resetting e2e database");
await runCommand(npxCmd, ["tsx", "scripts/reset-db.ts"], { cwd: e2eRoot, label: "db" }, logger.write);
logger.log("starting backend");
backend = startService(
goCmd,
["run", "./cmd/server"],
{
cwd: path.join(repoRoot, "backend"),
label: "backend",
env: childEnv({
SERVER_ADDRESS: ":9001",
DB_HOST: env.db.host,
DB_PORT: String(env.db.port),
DB_USER: env.db.user,
DB_PASSWORD: env.db.password,
DB_NAME: env.db.database,
JWT_SECRET: "clawmanager-e2e-secret",
RUNTIME_SCHEDULER_ENABLED: "false",
RUNTIME_AGENT_REPORT_TOKEN: env.runtimeAgentToken,
RUNTIME_WORKSPACE_ROOT: env.workspaceRoot,
OBJECT_STORAGE_LOCAL_FALLBACK: env.objectStorageFallback,
SKILL_SCANNER_ENABLED: "false"
})
},
logger.write
);
await waitForTcpPort("127.0.0.1", 9001, 120_000);
logger.log("starting frontend");
const frontendTarget = frontendHostAndPort();
frontend = startService(
npmCmd,
["run", "dev", "--", "--host", frontendTarget.host, "--port", frontendTarget.port, "--strictPort"],
{ cwd: path.join(repoRoot, "frontend"), label: "frontend", env: childEnv() },
logger.write
);
await waitForHttpStatus(env.frontendUrl, 200, 120_000);
logger.log("running Playwright");
const args = ["playwright", "test", ...suiteArgs(suite)];
if (ci && !extraArgs.some((value) => value.startsWith("--workers"))) {
args.push("--workers=1");
}
args.push(...extraArgs);
try {
await runCommand(
npxCmd,
args,
{
cwd: e2eRoot,
label: "playwright",
env: childEnv({
CI: ci ? "true" : process.env.CI,
E2E_HEADLESS: String(headless),
E2E_DEBUG: String(debug),
E2E_SLOW_MO_MS: String(slowMoMs)
})
},
logger.write
);
} catch (error) {
writeFailureScreenshotSummary(logger.write);
throw error;
}
logger.log("e2e run completed successfully");
} finally {
logger.log("cleaning up e2e services");
await stopService(frontend, "frontend");
await stopService(backend, "backend");
await runCommand(
"docker",
["compose", "-f", "docker/docker-compose.e2e.yml", "down", "--remove-orphans"],
{ cwd: e2eRoot, label: "docker" },
logger.write
).catch((error: unknown) => {
logger.log(`docker cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
});
await logger.close();
}
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}