import { spawnSync } from "node:child_process"; import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; import { LOCAL_BUILD_METADATA_DIST_PATHS } from "../../scripts/lib/local-build-metadata-paths.mjs"; const CHECK_SCRIPT = "scripts/check-openclaw-package-tarball.mjs"; function withTarball( inventory: string[], files: Record, testBody: (tarball: string) => void, version = "0.0.0", options: { includeControlUi?: boolean } = {}, ) { const root = mkdtempSync(join(tmpdir(), "openclaw-package-tarball-test-")); try { const packageRoot = join(root, "package"); mkdirSync(join(packageRoot, "dist"), { recursive: true }); writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name: "openclaw", version })); writeFileSync( join(packageRoot, "dist", "postinstall-inventory.json"), JSON.stringify(inventory), ); const tarFiles = options.includeControlUi === false ? files : { "dist/control-ui/index.html": "", "dist/control-ui/assets/app.js": "console.log('ok');\n", ...files, }; for (const [relativePath, body] of Object.entries(tarFiles)) { const filePath = join(packageRoot, relativePath); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, body); } const tarball = join(root, "openclaw.tgz"); const pack = spawnSync("tar", ["-czf", tarball, "-C", root, "package"], { encoding: "utf8", }); expect(pack.status, pack.stderr).toBe(0); testBody(tarball); } finally { rmSync(root, { recursive: true, force: true }); } } describe("check-openclaw-package-tarball", () => { it("allows legacy private QA inventory entries omitted from shipped tarballs through 2026.4.25", () => { withTarball( ["dist/index.js", "dist/extensions/qa-channel/runtime-api.js"], { "dist/index.js": "export {};\n" }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status, result.stderr).toBe(0); expect(result.stderr).toContain("legacy inventory references omitted private QA"); expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); }, "2026.4.25-beta.10", ); }); it("rejects legacy private QA inventory omissions for newer packages", () => { withTarball( ["dist/index.js", "dist/extensions/qa-channel/runtime-api.js"], { "dist/index.js": "export {};\n" }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain( "inventory references missing tar entry dist/extensions/qa-channel/runtime-api.js", ); expect(result.stderr).not.toContain("legacy inventory references omitted private QA"); }, "2026.4.26", ); }); it("still rejects non-legacy missing inventory entries", () => { withTarball( ["dist/index.js", "dist/cli.js"], { "dist/index.js": "export {};\n" }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain("inventory references missing tar entry dist/cli.js"); }, ); }); it("rejects dist files that import missing relative chunks", () => { withTarball( ["dist/cli/run-main.js"], { "dist/cli/run-main.js": 'await import("../memory-state-old.js");\n' }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain( "dist/cli/run-main.js imports missing dist/memory-state-old.js", ); }, "2026.4.27", ); }); it("accepts dist files whose relative chunks are present", () => { withTarball( ["dist/cli/run-main.js", "dist/memory-state-current.js"], { "dist/cli/run-main.js": 'await import("../memory-state-current.js");\n', "dist/memory-state-current.js": "export {};\n", }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status, result.stderr).toBe(0); expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); }, "2026.4.27", ); }); it("rejects imported dist chunks omitted from the postinstall inventory", () => { withTarball( ["dist/cli/run-main.js"], { "dist/cli/run-main.js": 'await import("../memory-state-current.js");\n', "dist/memory-state-current.js": "export {};\n", }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain( "inventory omits imported dist file dist/memory-state-current.js", ); }, "2026.4.27", ); }); it("rejects missing Control UI assets", () => { withTarball( ["dist/index.js"], { "dist/index.js": "export {};\n" }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain("missing required tar entry dist/control-ui/index.html"); expect(result.stderr).toContain( "missing required tar entries under dist/control-ui/assets/", ); }, "2026.4.27", { includeControlUi: false }, ); }); it("rejects local build metadata entries in package tarballs", () => { withTarball( ["dist/index.js", ...LOCAL_BUILD_METADATA_DIST_PATHS], { "dist/index.js": "export {};\n", ...Object.fromEntries(LOCAL_BUILD_METADATA_DIST_PATHS.map((entry) => [entry, "{}\n"])), }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status).not.toBe(0); expect(result.stderr).toContain( "forbidden local build metadata tar entry dist/.buildstamp", ); expect(result.stderr).toContain( "forbidden local build metadata tar entry dist/.runtime-postbuildstamp", ); }, "2026.4.27", ); }); it("allows local build metadata in already published legacy packages through 2026.4.26", () => { withTarball( ["dist/index.js", ...LOCAL_BUILD_METADATA_DIST_PATHS], { "dist/index.js": "export {};\n", ...Object.fromEntries(LOCAL_BUILD_METADATA_DIST_PATHS.map((entry) => [entry, "{}\n"])), }, (tarball) => { const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); expect(result.status, result.stderr).toBe(0); expect(result.stderr).toContain( "legacy package includes local build metadata tar entry dist/.buildstamp", ); expect(result.stderr).toContain( "legacy package includes local build metadata tar entry dist/.runtime-postbuildstamp", ); expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); }, "2026.4.26", ); }); });