diff --git a/package.json b/package.json index 4621d85..14d139b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "node --import tsx src/index.ts", "dev:continuous": "MONITOR_CONTINUOUS=true node --import tsx src/index.ts", "dev:ui": "UI_MODE=true node --import tsx src/index.ts", - "smoke:ui": "bash scripts/ui-smoke.sh", + "smoke:ui": "node scripts/ui-smoke.js", "command:backup-export": "APP_COMMAND=backup-export node --import tsx src/index.ts", "avatars:export": "node --import tsx scripts/export-staff-avatars.ts", "command:import-validate": "APP_COMMAND=import-validate node --import tsx src/index.ts", diff --git a/scripts/ui-smoke.js b/scripts/ui-smoke.js new file mode 100644 index 0000000..079246d --- /dev/null +++ b/scripts/ui-smoke.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Cross-platform UI smoke test. + * Replaces scripts/ui-smoke.sh for Windows compatibility. + */ +"use strict"; + +const { spawn } = require("node:child_process"); +const http = require("node:http"); +const path = require("node:path"); +const fs = require("node:fs"); + +const PORT = process.env.UI_SMOKE_PORT || "4516"; +const WAIT_SECONDS = Number(process.env.UI_SMOKE_WAIT_SECONDS || "10"); +const ROOT = path.resolve(__dirname, ".."); +const LOG_DIR = path.join(ROOT, "runtime"); +const LOG_FILE = path.join(LOG_DIR, `ui-smoke-${PORT}.log`); + +fs.mkdirSync(LOG_DIR, { recursive: true }); + +const logStream = fs.createWriteStream(LOG_FILE); +const child = spawn(process.execPath, ["--import", "tsx", "src/index.ts"], { + cwd: ROOT, + env: { ...process.env, UI_MODE: "true", UI_PORT: PORT }, + stdio: ["ignore", "pipe", "pipe"], +}); + +child.stdout.pipe(logStream); +child.stderr.pipe(logStream); + +function cleanup(code) { + try { child.kill(); } catch (_) { /* ignore */ } + process.exit(code); +} + +process.on("SIGINT", () => cleanup(1)); +process.on("SIGTERM", () => cleanup(1)); + +function fetch(urlPath) { + return new Promise((resolve, reject) => { + const req = http.get(`http://127.0.0.1:${PORT}${urlPath}`, (res) => { + let data = ""; + res.on("data", (c) => { data += c; }); + res.on("end", () => resolve(data)); + }); + req.on("error", reject); + req.setTimeout(3000, () => { req.destroy(); reject(new Error("timeout")); }); + }); +} + +async function waitForUI() { + const deadline = Date.now() + WAIT_SECONDS * 1000; + while (Date.now() < deadline) { + try { + await fetch("/"); + return; + } catch (_) { + await new Promise((r) => setTimeout(r, 1000)); + } + } + console.error(`UI smoke failed: server did not become ready within ${WAIT_SECONDS}s.`); + try { console.error(fs.readFileSync(LOG_FILE, "utf8")); } catch (_) { /* ignore */ } + cleanup(1); +} + +async function checkPage(urlPath, keywords, label) { + const body = await fetch(urlPath); + const found = keywords.some((kw) => body.includes(kw)); + if (!found) { + console.error(`FAIL: ${label} — none of [${keywords.join(", ")}] found in response (${body.length} bytes).`); + cleanup(1); + } +} + +async function main() { + await waitForUI(); + await checkPage("/", ["OpenClaw", "Control Center", "usage", "lang="], "GET /"); + await checkPage("/docs?lang=en", ["Open document workbench", "Control Center", "Docs"], "GET /docs?lang=en"); + console.log(`UI smoke passed on http://127.0.0.1:${PORT}`); + cleanup(0); +} + +main().catch((err) => { + console.error("UI smoke error:", err); + cleanup(1); +}); diff --git a/test/oss-readiness.test.ts b/test/oss-readiness.test.ts index 5b0fc06..d8c0676 100644 --- a/test/oss-readiness.test.ts +++ b/test/oss-readiness.test.ts @@ -11,13 +11,11 @@ const PUBLIC_FILES = [ "README.md", "docs/ARCHITECTURE.md", "docs/PROGRESS.md", - "docs/mission.runbook.md", + "docs/mission-control-runbook-v2.md", "ecosystem.config.cjs", - "mission.config.json", "scripts/run_verifier.sh", "scripts/mc_dod_evaluator.py", "scripts/mc_rollback_plan.py", - "workflows/pandas_v3_autopilot.lobster", ]; test("repo includes baseline open-source release metadata", () => { @@ -25,9 +23,9 @@ test("repo includes baseline open-source release metadata", () => { assert(existsSync(path.join(ROOT, "LICENSE")), "Expected LICENSE to exist."); const ignore = readFileSync(path.join(ROOT, ".gitignore"), "utf8"); - assert.match(ignore, /(^|\n)node_modules\/(\n|$)/); - assert.match(ignore, /(^|\n)dist\/(\n|$)/); - assert.match(ignore, /(^|\n)\/?runtime\/(\n|$)/); + assert.match(ignore, /(^|\r?\n)node_modules\/(\r?\n|$)/); + assert.match(ignore, /(^|\r?\n)dist\/(\r?\n|$)/); + assert.match(ignore, /(^|\r?\n)\/?runtime\/(\r?\n|$)/); const license = readFileSync(path.join(ROOT, "LICENSE"), "utf8"); assert.match(license, /MIT License/); diff --git a/test/phase9-routes-commands.test.ts b/test/phase9-routes-commands.test.ts index 0e03efe..3bcf207 100644 --- a/test/phase9-routes-commands.test.ts +++ b/test/phase9-routes-commands.test.ts @@ -121,10 +121,11 @@ test("done-checklist route logic builds readiness payload", async () => { }); test("backup export command path writes bundle and operation audit entry", async () => { - const run = await runCommand("npm", ["run", "command:backup-export"], { + const run = await runCommand(process.execPath, ["--import", "tsx", "src/index.ts"], { cwd: PROJECT_ROOT, env: { ...process.env, + APP_COMMAND: "backup-export", READONLY_MODE: "true", APPROVAL_ACTIONS_ENABLED: "false", APPROVAL_ACTIONS_DRY_RUN: "true", @@ -148,7 +149,7 @@ test("backup export command path writes bundle and operation audit entry", async const metadata = asRecord(commandEntry.metadata); assert.equal(typeof metadata?.path, "string"); const writtenPath = metadata?.path as string; - assert(writtenPath.includes("/runtime/exports/")); + assert(writtenPath.replace(/\\/g, "/").includes("/runtime/exports/")); assert(await fileExists(writtenPath), "Expected backup export file path from audit metadata to exist."); }); @@ -182,10 +183,11 @@ test("acks-prune command removes expired records and writes audit entry", async await writeFile(ACKS_PATH, `${JSON.stringify(fixture, null, 2)}\n`, "utf8"); try { - const run = await runCommand("npm", ["run", "command:acks-prune"], { + const run = await runCommand(process.execPath, ["--import", "tsx", "src/index.ts"], { cwd: PROJECT_ROOT, env: { ...process.env, + APP_COMMAND: "acks-prune", READONLY_MODE: "true", APPROVAL_ACTIONS_ENABLED: "false", APPROVAL_ACTIONS_DRY_RUN: "true", diff --git a/test/ui-render-smoke.test.ts b/test/ui-render-smoke.test.ts index 9b08e1d..89f2acf 100644 --- a/test/ui-render-smoke.test.ts +++ b/test/ui-render-smoke.test.ts @@ -388,7 +388,7 @@ test("usage dashboard includes token type share and cron token share sections", }); test("memory and workspace sections expose editable file workbenches", async () => { - const source = await readFile("src/ui/server.ts", "utf8"); + const source = (await readFile("src/ui/server.ts", "utf8")).replace(/\r\n/g, "\n"); assert(source.includes("/api/files")); assert(source.includes("/api/files/content")); assert(source.includes("scope must be one of: memory, workspace")); diff --git a/test/usage-cost.test.ts b/test/usage-cost.test.ts index 37d92a0..c1b88e4 100644 --- a/test/usage-cost.test.ts +++ b/test/usage-cost.test.ts @@ -181,7 +181,7 @@ test("usage-cost snapshot backfills subscription consumed from runtime usage whe assert.equal(usage.subscription.limit, undefined); assert(usage.subscription.detail.includes("Provider remaining/limit cannot be derived")); assert.equal(usage.subscription.reasonCode, "runtime_backfill_only"); - assert(usage.subscription.sourcePath?.includes("sessions/*.jsonl")); + assert(usage.subscription.sourcePath?.replace(/\\/g, "/").includes("sessions/*.jsonl")); assert.equal(usage.connectors.subscriptionUsage, "not_connected"); assert( usage.connectors.todos.some(