import { spawnSync } from "node:child_process"; import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../../scripts/lib/workspace-bootstrap-smoke.mts"; const CONTROL_UI_INDEX = "dist/control-ui/index.html"; const CODE_MODE_WORKER_PATH = "dist/agents/code-mode.worker.js"; const CONTROL_UI_ASSETS = [ "dist/control-ui/assets/app.js", "dist/control-ui/assets/app.js.br", "dist/control-ui/assets/app.js.gz", ] as const; const CONTROL_UI_FILES = [CONTROL_UI_INDEX, ...CONTROL_UI_ASSETS]; const CHECK_SCRIPT = resolve("scripts/check-openclaw-package-tarball.mjs"); const TYPESCRIPT_PACKAGE_ROOT = fileURLToPath( new URL("../../node_modules/typescript", import.meta.url), ); function writeFixtureFile(packageRoot: string, relativePath: string, content: string): void { const filePath = join(packageRoot, relativePath); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, content); } function withPackedPackage( inventory: readonly string[], run: (fixture: { root: string; tarball: string }) => void, options: { postinstall?: boolean } = {}, ): void { const root = mkdtempSync(join(tmpdir(), "openclaw-control-ui-package-inventory-")); const packageRoot = join(root, "package"); try { mkdirSync(packageRoot, { recursive: true }); const version = "2026.7.2"; const typescriptRoot = resolve("node_modules/typescript"); const typescriptVersion = ( JSON.parse(readFileSync(join(typescriptRoot, "package.json"), "utf8")) as { version: string; } ).version; writeFixtureFile( packageRoot, "package.json", JSON.stringify({ name: "openclaw", version, type: "module", ...(options.postinstall === false ? {} : { scripts: { postinstall: "node scripts/postinstall-bundled-plugins.mjs" }, dependencies: { typescript: typescriptVersion }, bundledDependencies: ["typescript"], }), }), ); writeFixtureFile( packageRoot, "dist/postinstall-inventory.json", JSON.stringify([...new Set([...inventory, CODE_MODE_WORKER_PATH])]), ); writeFixtureFile(packageRoot, "dist/index.js", "export {};\n"); writeFixtureFile(packageRoot, CODE_MODE_WORKER_PATH, "export {};\n"); writeFixtureFile( packageRoot, CONTROL_UI_INDEX, '\n', ); for (const assetPath of CONTROL_UI_ASSETS) { writeFixtureFile(packageRoot, assetPath, "shipped Control UI asset\n"); } writeFixtureFile( packageRoot, "dist/openclaw-install-guard", "OpenClaw package preinstall has not completed.\n", ); for (const relativePath of WORKSPACE_TEMPLATE_PACK_PATHS) { writeFixtureFile(packageRoot, relativePath, `# ${relativePath}\n`); } for (const relativePath of [ "scripts/postinstall-bundled-plugins.mjs", "scripts/lib/guard-inventory-utils.mjs", "scripts/lib/package-dist-imports.mjs", ]) { const destination = join(packageRoot, relativePath); mkdirSync(dirname(destination), { recursive: true }); copyFileSync(resolve(relativePath), destination); } if (options.postinstall !== false) { // Offline npm must exercise the same bundled TypeScript AST dependency // that the real postinstall uses to preserve its complete import graph. cpSync(typescriptRoot, join(packageRoot, "node_modules/typescript"), { recursive: true, }); } const packed = spawnSync( "npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", root], { cwd: packageRoot, encoding: "utf8", timeout: 30_000 }, ); expect(packed.status, packed.stderr || packed.error?.message).toBe(0); // npm <=11 `pack --json` emits an array; npm 12 emits an object keyed by package name. const parsed = JSON.parse(packed.stdout) as | { filename: string }[] | Record; const packResult = Array.isArray(parsed) ? parsed[0] : Object.values(parsed)[0]; const filename = packResult?.filename ?? ""; const tarball = join(root, filename); expect(existsSync(tarball)).toBe(true); run({ root, tarball }); } finally { rmSync(root, { recursive: true, force: true }); } } function checkPackedPackage(tarball: string) { return spawnSync(process.execPath, [CHECK_SCRIPT, tarball], { encoding: "utf8", timeout: 30_000, }); } function installPackedPackage(root: string, tarball: string) { const home = join(root, "home"); const state = join(root, "state"); const temporary = join(root, "tmp"); for (const directory of [home, state, temporary]) { mkdirSync(directory, { recursive: true }); } const installRoot = join(root, "installed"); mkdirSync(installRoot, { recursive: true }); writeFileSync( join(installRoot, "package.json"), `${JSON.stringify({ private: true }, null, 2)}\n`, ); const result = spawnSync( "npm", [ "install", "--foreground-scripts", "--dangerously-allow-all-scripts", "--prefix", installRoot, "--no-audit", "--no-fund", "--offline", TYPESCRIPT_PACKAGE_ROOT, tarball, ], { encoding: "utf8", timeout: 30_000, env: { ...process.env, HOME: home, OPENCLAW_HOME: home, OPENCLAW_STATE_DIR: state, OPENCLAW_CONFIG_PATH: join(state, "openclaw.json"), TMPDIR: temporary, TMP: temporary, TEMP: temporary, }, }, ); expect(result.status, result.stderr || result.error?.message).toBe(0); return join(installRoot, "node_modules", "openclaw"); } describe("packaged Control UI postinstall inventory", () => { it.each(CONTROL_UI_FILES)( "rejects a real npm package when postinstall would delete %s", (omittedFile) => { const inventory = ["dist/index.js", ...CONTROL_UI_FILES].filter( (relativePath) => relativePath !== omittedFile, ); withPackedPackage(inventory, ({ tarball }) => { const result = checkPackedPackage(tarball); expect(result.status, result.stdout).not.toBe(0); expect(result.stderr).toContain( `postinstall inventory omits Control UI file ${omittedFile}`, ); }); }, ); it("proves actual npm postinstall deletes an omitted dashboard from a falsely accepted package", () => { withPackedPackage(["dist/index.js"], ({ root, tarball }) => { const validation = checkPackedPackage(tarball); const installedPackageRoot = installPackedPackage(root, tarball); expect(existsSync(join(installedPackageRoot, "dist/index.js"))).toBe(true); for (const relativePath of CONTROL_UI_FILES) { expect(existsSync(join(installedPackageRoot, relativePath))).toBe(false); } expect(validation.status, validation.stdout).not.toBe(0); expect(validation.stderr).toContain( "postinstall inventory omits Control UI file dist/control-ui/index.html", ); }); }); it("accepts a packaged dashboard whose complete UI survives postinstall", () => { withPackedPackage(["dist/index.js", ...CONTROL_UI_FILES], ({ root, tarball }) => { const result = checkPackedPackage(tarball); expect(result.status, result.stderr).toBe(0); expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); const installedPackageRoot = installPackedPackage(root, tarball); expect(existsSync(join(installedPackageRoot, CODE_MODE_WORKER_PATH))).toBe(true); for (const relativePath of CONTROL_UI_FILES) { expect(existsSync(join(installedPackageRoot, relativePath))).toBe(true); } }); }); it("does not impose postinstall inventory rules on lifecycle-free archived fixtures", () => { withPackedPackage( ["dist/index.js"], ({ tarball }) => { const result = checkPackedPackage(tarball); expect(result.status, result.stderr).toBe(0); }, { postinstall: false }, ); }); });