mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 08:01:37 +00:00
243 lines
8.2 KiB
TypeScript
243 lines
8.2 KiB
TypeScript
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.mjs";
|
|
|
|
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,
|
|
'<!doctype html><script type="module" src="./assets/app.js"></script>\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<string, { filename: string }>;
|
|
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 },
|
|
);
|
|
});
|
|
});
|