Files
openclaw/test/scripts/output-root-guard.test.ts
wangyan2026 d865c9b8f5 fix(build): fail closed on symlinked build output roots (#116705)
Reject symlinked generated-output roots before build and postbuild mutation paths can recurse into unrelated targets. Cover aggregate, UI, export-template, bundled-plugin, Docker-prune, and plugin-runtime entry points with regression tests and migration guidance.

Closes #116498

Co-authored-by: WangYan <wang.yan29@xydigit.com>
2026-07-31 19:37:05 +08:00

77 lines
2.9 KiB
TypeScript

// Output Root Guard tests cover the shared build output-root symlink guard.
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { assertRealOutputRoot } from "../../scripts/lib/output-root-guard.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
describe("assertRealOutputRoot", () => {
it("accepts a missing output root", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
expect(() => assertRealOutputRoot(path.join(rootDir, "dist"))).not.toThrow();
});
it("propagates failures other than a missing output root", () => {
const failure = Object.assign(new Error("permission denied"), { code: "EACCES" });
const fsImpl = {
...fs,
lstatSync: () => {
throw failure;
},
};
expect(() => assertRealOutputRoot("/unreadable/dist", { fs: fsImpl })).toThrow(failure);
});
it("accepts a real directory output root", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
fs.mkdirSync(path.join(rootDir, "dist"));
expect(() => assertRealOutputRoot(path.join(rootDir, "dist"))).not.toThrow();
});
it("accepts a plain file output root", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
const distPath = path.join(rootDir, "dist");
fs.writeFileSync(distPath, "stale\n");
expect(() => assertRealOutputRoot(distPath)).not.toThrow();
});
it("rejects a symlinked output root and names the remediation", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
const targetDir = path.join(rootDir, "gateway-dist");
fs.mkdirSync(targetDir);
const distLink = path.join(rootDir, "dist");
fs.symlinkSync(targetDir, distLink, "dir");
expect(() => assertRealOutputRoot(distLink)).toThrow(
/symbolic link.*Remove the symlink or replace it with a real directory/su,
);
expect(fs.readlinkSync(distLink)).toBe(targetDir);
});
it("rejects a dangling symlinked output root", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
const distLink = path.join(rootDir, "dist");
fs.symlinkSync(path.join(rootDir, "missing-target"), distLink, "dir");
expect(() => assertRealOutputRoot(distLink)).toThrow(/symbolic link/u);
expect(fs.lstatSync(distLink).isSymbolicLink()).toBe(true);
});
it.runIf(process.platform === "win32")("rejects an NTFS junction output root", () => {
const rootDir = createTempDir("openclaw-output-root-guard-");
const targetDir = path.join(rootDir, "gateway-dist");
fs.mkdirSync(targetDir);
const distLink = path.join(rootDir, "dist");
fs.symlinkSync(targetDir, distLink, "junction");
expect(() => assertRealOutputRoot(distLink)).toThrow(/symbolic link/u);
expect(fs.lstatSync(distLink).isSymbolicLink()).toBe(true);
});
});