Files
openclaw/test/scripts/runtime-postbuild.test.ts
Peter Steinberger 2bdf8709fc test(core): remove low-value tests (batch d009) (#158725)
* test(codex): deslop s003 tests

* test(gateway): deslop s010 tests

* test(agents): deslop s013 tests

* test(commands): deslop s011 tests

* test(gateway): deslop s014 tests

* test(gateway): deslop s019 tests

* test(infra): deslop s016 tests

* test(agents): deslop s020 tests

* test(scripts): deslop s017 tests

* test(auto-reply): deslop s021 tests

* test: align d009 replay with current fixture contracts

* test(agents): remove orphaned BTW fixture export

* test: preserve independent regression coverage in d009

Restore eight independent authentication, retry, transcript, goal, reply ownership, and Xcode coverage contracts identified during review. Final correction review passed; final Testbox validation remains pending after lease and transport failures.
2026-09-26 11:08:36 +00:00

1978 lines
75 KiB
TypeScript

// Runtime Postbuild tests cover runtime postbuild script behavior.
import childProcess from "node:child_process";
import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { afterAll, describe, expect, it, vi } from "vitest";
import { createNativeTypeScriptParser } from "../../scripts/lib/native-typescript.mts";
import {
copyStaticExtensionAssets,
copyStaticExtensionAssetsToRuntimeOverlay,
discoverStaticExtensionAssets,
} from "../../scripts/lib/static-extension-assets.mts";
import {
parseUpdateCompatibilityInventory,
readUpdateCompatibilityInventory,
recordUpdateCompatibilityRelease,
writeUpdateCompatibilityChunks,
type UpdateCompatibilityInventory,
type UpdateCompatibilityRelease,
} from "../../scripts/lib/update-compat-chunks.mts";
import { buildUpdateConfigRuntimeAlias } from "../../scripts/lib/update-config-runtime-compat.mts";
import {
rewriteRootRuntimeImportsToStableAliases,
runRuntimePostBuild,
writeLegacyCliExitCompatChunks,
writeLegacyRootRuntimeCompatAliases,
writeStableRootRuntimeAliases,
} from "../../scripts/runtime-postbuild.mts";
import { expectNoNodeFsScans } from "../../src/test-utils/fs-scan-assertions.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import { readBuildIdFromBuildInfoForModuleUrl } from "../../src/version.js";
import { createScriptTestHarness } from "./test-helpers.js";
const parser = createNativeTypeScriptParser();
afterAll(() => parser.close());
const testNodeExecPath = resolveTestNodeExecPath();
import {
previousReleaseInventory,
writeUpdateCompatibilityBuildFixture,
} from "./update-compat-chunks.test-support.js";
const { createTempDir } = createScriptTestHarness();
const MODULE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
async function expectPathMissing(targetPath: string): Promise<void> {
let statError: unknown;
try {
await fs.stat(targetPath);
} catch (error) {
statError = error;
}
expect(statError).toBeInstanceOf(Error);
if (!(statError instanceof Error)) {
throw new Error("expected missing path error");
}
expect(Reflect.get(statError, "code")).toBe("ENOENT");
}
async function writeExportHtmlBuildFixture(rootDir: string): Promise<void> {
const sourceDir = path.join(rootDir, "src", "auto-reply", "reply", "export-html");
await fs.mkdir(sourceDir, { recursive: true });
await fs.writeFile(path.join(sourceDir, "template.html"), "<html></html>\n", "utf8");
await fs.writeFile(path.join(rootDir, "package.json"), "{}\n", "utf8");
for (const fixture of [
{
name: "marked",
exports: { ".": "./index.js", "./package.json": "./package.json" },
source: 'export const parse = () => "alternate-root-marked"; export const use = () => {};\n',
license: "ALTERNATE ROOT MARKED LICENSE\n",
},
{
name: "highlight.js",
exports: { "./lib/common": "./common.js", "./package.json": "./package.json" },
source: 'export default { marker: "alternate-root-highlight" };\n',
license: "ALTERNATE ROOT HIGHLIGHT LICENSE\n",
},
]) {
const packageDir = path.join(rootDir, "node_modules", fixture.name);
await fs.mkdir(packageDir, { recursive: true });
await fs.writeFile(
path.join(packageDir, "package.json"),
`${JSON.stringify({ name: fixture.name, type: "module", exports: fixture.exports })}\n`,
"utf8",
);
const entryName = fixture.name === "marked" ? "index.js" : "common.js";
await fs.writeFile(path.join(packageDir, entryName), fixture.source, "utf8");
await fs.writeFile(path.join(packageDir, "LICENSE"), fixture.license, "utf8");
}
}
describe("runtime postbuild static assets", () => {
it("copies bundled hook metadata without replacing compiled handlers", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-hooks-");
writeUpdateCompatibilityBuildFixture(rootDir);
const sourceHookDir = path.join(rootDir, "src", "hooks", "bundled", "session-memory");
const distHookDir = path.join(rootDir, "dist", "bundled", "session-memory");
await fs.mkdir(sourceHookDir, { recursive: true });
await fs.mkdir(distHookDir, { recursive: true });
await fs.writeFile(path.join(sourceHookDir, "HOOK.md"), "---\nname: session-memory\n---\n");
await fs.writeFile(path.join(distHookDir, "handler.js"), "export default () => {};\n");
runRuntimePostBuild({
rootDir,
env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0" },
timings: false,
});
await expect(fs.readFile(path.join(distHookDir, "HOOK.md"), "utf8")).resolves.toContain(
"name: session-memory",
);
await expect(fs.readFile(path.join(distHookDir, "handler.js"), "utf8")).resolves.toBe(
"export default () => {};\n",
);
});
it("discovers repo static asset metadata without scanning extension directories", () => {
const payload = expectNoNodeFsScans<{
outputs: string[];
sources: string[];
packageOutputs: string[];
}>(`
const assets = await import("./scripts/lib/static-extension-assets.mts");
const discovered = assets.discoverStaticExtensionAssets();
const packageAssets = assets.discoverStaticExtensionAssets({ includeExternalPlugins: true });
return {
outputs: discovered.map(({ dest }) => dest),
sources: assets.listStaticExtensionAssetSources(),
packageOutputs: packageAssets.map(({ dest }) => dest),
};
`);
expect(payload.outputs).toEqual([
"dist/extensions/acpx/mcp-command-line.mjs",
"dist/extensions/acpx/mcp-proxy.mjs",
"dist/extensions/apple-fm/assets/AppleFoundationModels.swift",
"dist/extensions/code-mode-quickjs/assets/encoding.so",
"dist/extensions/code-mode-quickjs/assets/quickjs.wasm",
"dist/extensions/crabbox/assets/openclaw-worker-wallpaper.png",
"dist/extensions/onepassword/onepassword-op-path.js",
"dist/extensions/onepassword/onepassword-secret-id.js",
"dist/extensions/onepassword/onepassword-secret-ref-resolver.js",
"dist/extensions/vault/vault-secret-id.js",
"dist/extensions/vault/vault-secret-ref-resolver.js",
]);
expect(payload.sources).not.toContain(
"extensions/diffs-language-pack/assets/viewer-runtime.js",
);
expect(payload.sources).not.toContain("extensions/diffs/assets/viewer-runtime.js");
expect(payload.sources).not.toContain("extensions/discord/assets/embedded-app-sdk.mjs");
expect(payload.packageOutputs).toContain("dist/extensions/discord/assets/embedded-app-sdk.mjs");
expect(payload.sources).toContain("extensions/crabbox/assets/openclaw-worker-wallpaper.png");
});
it("copies each package asset once with multiple Git index stages", async () => {
const rootDir = createTempDir("openclaw-static-assets-index-");
const git = (args: string[], input?: string) =>
childProcess.execFileSync("git", args, { cwd: rootDir, encoding: "utf8", input });
const pluginIds = ["conflicted", "package-only", "with-manifest"];
for (const id of pluginIds) {
const pluginDir = path.join(rootDir, "extensions", id);
await fs.mkdir(pluginDir, { recursive: true });
await fs.writeFile(path.join(pluginDir, "asset.txt"), `${id} bytes\n`);
await fs.writeFile(
path.join(pluginDir, "package.json"),
JSON.stringify({
openclaw: { build: { staticAssets: [{ source: "asset.txt", output: "asset.txt" }] } },
}),
);
}
for (const id of ["with-manifest", "manifest-only"]) {
const pluginDir = path.join(rootDir, "extensions", id);
await fs.mkdir(pluginDir, { recursive: true });
await fs.writeFile(path.join(pluginDir, "openclaw.plugin.json"), "not valid JSON");
}
git(["init", "-q"]);
git(["add", "extensions"]);
const packagePath = "extensions/conflicted/package.json";
const blob = git(["hash-object", "-w", "--stdin"], "{}").trim();
// Keep resolved working-tree metadata while the index still holds all merge stages.
git(
["update-index", "--index-info"],
[
`0 ${"0".repeat(blob.length)}\t${packagePath}`,
...[1, 2, 3].map((stage) => `100644 ${blob} ${stage}\t${packagePath}`),
"",
].join("\n"),
);
expect(git(["ls-files", "--", packagePath]).trim().split("\n")).toHaveLength(3);
const copy = vi.spyOn(fsSync, "copyFileSync");
try {
copyStaticExtensionAssets({ rootDir });
expect(copy).toHaveBeenCalledTimes(3);
expect(discoverStaticExtensionAssets({ rootDir }).map(({ pluginDir }) => pluginDir)).toEqual(
pluginIds,
);
for (const id of pluginIds) {
await expect(
fs.readFile(path.join(rootDir, "dist", "extensions", id, "asset.txt"), "utf8"),
).resolves.toBe(`${id} bytes\n`);
}
} finally {
copy.mockRestore();
}
});
it.each([
{ name: "top-level array", packageJson: [] },
{ name: "array openclaw section", packageJson: { openclaw: [] } },
{ name: "array build section", packageJson: { openclaw: { build: [] } } },
{
name: "non-record asset entries",
packageJson: {
openclaw: {
build: {
staticAssets: [[], "asset", null, { source: 42, output: [] }],
},
},
},
},
])("ignores malformed $name metadata", async ({ packageJson }) => {
const rootDir = createTempDir("openclaw-runtime-postbuild-malformed-");
const packageDir = path.join(rootDir, "extensions", "demo");
await fs.mkdir(packageDir, { recursive: true });
await fs.writeFile(path.join(packageDir, "package.json"), JSON.stringify(packageJson), "utf8");
expect(discoverStaticExtensionAssets({ rootDir })).toEqual([]);
});
it.each([
{ name: "normal root build", params: {}, included: false },
{ name: "isolated external build", params: { includeExternalPlugins: true }, included: true },
{
name: "Docker-selected build",
params: { env: { OPENCLAW_INTERNAL_DOCKER_BUILD_PLUGIN_IDS: "external-demo" } },
included: true,
},
])("$name handles external plugin assets", async ({ params, included }) => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const packageDir = path.join(rootDir, "extensions", "external-demo");
await fs.mkdir(packageDir, { recursive: true });
await fs.writeFile(
path.join(packageDir, "package.json"),
JSON.stringify({
name: "@openclaw/external-demo",
openclaw: {
build: {
bundledDist: false,
staticAssets: [
{
source: "./assets/runtime.js",
output: "assets/runtime.js",
},
],
},
},
}),
"utf8",
);
expect(discoverStaticExtensionAssets({ rootDir, ...params })).toEqual(
included
? [
{
pluginDir: "external-demo",
src: "extensions/external-demo/assets/runtime.js",
dest: "dist/extensions/external-demo/assets/runtime.js",
},
]
: [],
);
});
it.each([
{ name: "package-relative source", dependency: "", local: false, missing: false },
{ name: "hoisted dependency", dependency: "engine", local: false, missing: false },
{
name: "hoisted scoped dependency",
dependency: "@fixture/engine",
local: false,
missing: false,
},
{
name: "plugin-local dependency precedence",
dependency: "@fixture/engine",
local: true,
missing: false,
},
{
name: "missing asset in selected dependency",
dependency: "@fixture/engine",
local: true,
missing: true,
},
])("stages $name during the same postbuild run", async ({ dependency, local, missing }) => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const output = "assets/viewer-runtime.js";
const source = dependency ? `node_modules/${dependency}/private/runtime.js` : output;
const packageDir = path.join(rootDir, "extensions", "diffs");
const distAsset = "dist/extensions/diffs/assets/viewer-runtime.js";
const runtimeAsset = "dist-runtime/extensions/diffs/assets/viewer-runtime.js";
const contents = "export const viewer = true;\n";
const warn = vi.fn();
await fs.mkdir(path.join(rootDir, "extensions", "diffs", "assets"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "extensions", "diffs", "package.json"),
JSON.stringify({
name: "@openclaw/diffs",
...(dependency ? { dependencies: { [dependency]: "1.0.0" } } : {}),
openclaw: {
extensions: ["./index.ts"],
build: {
staticAssets: [{ source: `./${source}`, output }],
},
},
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "extensions", "diffs", "openclaw.plugin.json"),
'{"id":"diffs"}\n',
"utf8",
);
if (dependency) {
for (const base of local ? [rootDir, packageDir] : [rootDir]) {
const dependencyDir = path.join(base, "node_modules", dependency);
await fs.mkdir(path.join(dependencyDir, "private"), { recursive: true });
await fs.writeFile(
path.join(dependencyDir, "package.json"),
JSON.stringify({ name: dependency, exports: { "./runtime": "./private/runtime.js" } }),
);
if (!(missing && base === packageDir)) {
await fs.writeFile(
path.join(dependencyDir, "private/runtime.js"),
local && base === rootDir ? "wrong ancestor version\n" : contents,
);
}
}
} else {
await fs.writeFile(path.join(packageDir, source), contents);
}
writeUpdateCompatibilityBuildFixture(rootDir);
runRuntimePostBuild({
cwd: rootDir,
repoRoot: rootDir,
rootDir,
timings: false,
warn,
});
for (const asset of [distAsset, runtimeAsset]) {
if (missing) {
await expectPathMissing(path.join(rootDir, asset));
} else {
await expect(fs.readFile(path.join(rootDir, asset), "utf8")).resolves.toBe(contents);
}
}
if (missing) {
expect(warn).toHaveBeenCalledWith(expect.stringContaining("static asset not found"));
}
});
it("writes every phase beneath the cwd-only caller root", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-cwd-");
await writeExportHtmlBuildFixture(rootDir);
writeUpdateCompatibilityBuildFixture(rootDir);
runRuntimePostBuild({
cwd: rootDir,
env: {
OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0",
OPENCLAW_CONTROL_UI_BUILD_ID: "source-runtime-build",
},
timings: false,
});
expect(
readBuildIdFromBuildInfoForModuleUrl(pathToFileURL(path.join(rootDir, "dist/entry.js")).href),
).toBe("source-runtime-build");
await expect(
fs.readFile(path.join(rootDir, "dist", "export-html", "template.html"), "utf8"),
).resolves.toBe("<html></html>\n");
const vendorDir = path.join(rootDir, "dist", "export-html", "vendor");
const markedAsset = await fs.readFile(path.join(vendorDir, "marked.min.js"), "utf8");
const highlightAsset = await fs.readFile(path.join(vendorDir, "highlight.min.js"), "utf8");
expect(markedAsset).toContain("ALTERNATE ROOT MARKED LICENSE");
expect(markedAsset).toContain("alternate-root-marked");
expect(highlightAsset).toContain("ALTERNATE ROOT HIGHLIGHT LICENSE");
expect(highlightAsset).toContain("alternate-root-highlight");
await expect(
fs.readFile(path.join(rootDir, "dist", "channel-catalog.json"), "utf8"),
).resolves.toContain('"entries"');
const bridge = await import(
pathToFileURL(path.join(rootDir, "dist", "shared-Y6bNiw2w.js")).href
);
expect(bridge.resolveNodeRunner()).toBe(process.versions.bun ? "node" : process.execPath);
});
it("uses rootDir ahead of conflicting cwd and repoRoot for every phase", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-root-");
const cwd = createTempDir("openclaw-runtime-postbuild-rejected-cwd-");
const repoRoot = createTempDir("openclaw-runtime-postbuild-rejected-repo-");
await writeExportHtmlBuildFixture(rootDir);
writeUpdateCompatibilityBuildFixture(rootDir);
runRuntimePostBuild({
cwd,
env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0" },
repoRoot,
rootDir,
timings: false,
});
await expect(
fs.readFile(path.join(rootDir, "dist", "export-html", "template.html"), "utf8"),
).resolves.toBe("<html></html>\n");
await expect(
fs.readFile(path.join(rootDir, "dist", "channel-catalog.json"), "utf8"),
).resolves.toContain('"entries"');
await expect(
fs.readFile(path.join(rootDir, "dist", "shared-Y6bNiw2w.js"), "utf8"),
).resolves.toContain("resolveNodeRunner");
await expectPathMissing(path.join(cwd, "dist"));
await expectPathMissing(path.join(repoRoot, "dist"));
});
it("validates every postbuild root before running any phase", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-roots-");
const distFile = path.join(rootDir, "dist", "keep.js");
const targetDir = path.join(rootDir, "gateway-runtime");
await fs.mkdir(path.dirname(distFile), { recursive: true });
await fs.mkdir(targetDir);
await fs.writeFile(distFile, "keep\n");
await fs.symlink(targetDir, path.join(rootDir, "dist-runtime"), "dir");
expect(() =>
runRuntimePostBuild({
cwd: rootDir,
repoRoot: rootDir,
rootDir,
timings: false,
}),
).toThrow(/symbolic link/u);
await expect(fs.readdir(path.join(rootDir, "dist"))).resolves.toEqual(["keep.js"]);
await expect(fs.readFile(distFile, "utf8")).resolves.toBe("keep\n");
});
it("preserves restored dist static assets when plugin sources are absent", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const output = "assets/viewer-runtime.js";
const distPluginDir = path.join(rootDir, "dist", "extensions", "diffs");
const runtimeAsset = path.join(rootDir, "dist-runtime", "extensions", "diffs", output);
await fs.mkdir(path.join(distPluginDir, "assets"), { recursive: true });
await fs.writeFile(path.join(distPluginDir, "index.js"), "export default {};\n", "utf8");
await fs.writeFile(
path.join(distPluginDir, "openclaw.plugin.json"),
'{"id":"diffs"}\n',
"utf8",
);
await fs.writeFile(
path.join(distPluginDir, "package.json"),
JSON.stringify({
name: "@openclaw/diffs",
openclaw: {
extensions: ["./index.js"],
build: {
staticAssets: [{ source: `./${output}`, output }],
},
},
}),
"utf8",
);
await fs.writeFile(path.join(distPluginDir, output), "console.log('viewer');\n", "utf8");
writeUpdateCompatibilityBuildFixture(rootDir);
runRuntimePostBuild({
cwd: rootDir,
repoRoot: rootDir,
rootDir,
timings: false,
});
await expect(fs.readFile(runtimeAsset, "utf8")).resolves.toBe("console.log('viewer');\n");
});
it("can skip static asset copies for minimal runtime builds", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
writeUpdateCompatibilityBuildFixture(rootDir);
const warn = vi.fn();
const output = "assets/viewer-runtime.js";
await fs.mkdir(path.join(rootDir, "extensions", "diffs"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "extensions", "diffs", "package.json"),
JSON.stringify({
name: "@openclaw/diffs",
openclaw: {
extensions: ["./index.ts"],
build: {
staticAssets: [{ source: `./${output}`, output }],
},
},
}),
"utf8",
);
runRuntimePostBuild({
cwd: rootDir,
repoRoot: rootDir,
rootDir,
env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0" },
timings: false,
warn,
});
expect(warn).not.toHaveBeenCalled();
await expectPathMissing(path.join(rootDir, "dist", "extensions", "diffs", output));
});
it("skips runtime overlay asset copies when the runtime extension root is absent", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
await fs.mkdir(path.join(rootDir, "extensions", "demo", "assets"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "extensions", "demo", "assets", "viewer.js"),
"viewer\n",
"utf8",
);
copyStaticExtensionAssetsToRuntimeOverlay({
rootDir,
assets: [
{
src: "extensions/demo/assets/viewer.js",
dest: "dist/extensions/demo/assets/viewer.js",
},
],
});
await expectPathMissing(path.join(rootDir, "dist-runtime", "extensions", "demo", "assets"));
});
it("ignores runtime overlay static assets outside dist extensions", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
await fs.mkdir(path.join(rootDir, "dist-runtime", "extensions"), { recursive: true });
await fs.mkdir(path.join(rootDir, "extensions", "demo", "assets"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "extensions", "demo", "assets", "viewer.js"),
"viewer\n",
"utf8",
);
copyStaticExtensionAssetsToRuntimeOverlay({
rootDir,
assets: [
{
src: "extensions/demo/assets/viewer.js",
dest: "dist/other/demo/assets/viewer.js",
},
],
});
await expectPathMissing(path.join(rootDir, "dist-runtime", "other", "demo", "assets"));
});
it("warns when a runtime overlay static asset source is missing", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const warn = vi.fn();
await fs.mkdir(path.join(rootDir, "dist-runtime", "extensions"), { recursive: true });
copyStaticExtensionAssetsToRuntimeOverlay({
rootDir,
assets: [
{
src: "extensions/demo/assets/missing.js",
dest: "dist/extensions/demo/assets/missing.js",
},
],
warn,
});
expect(warn).toHaveBeenCalledWith(
"[runtime-postbuild] static asset not found, skipping: extensions/demo/assets/missing.js",
);
await expectPathMissing(
path.join(rootDir, "dist-runtime", "extensions", "demo", "assets", "missing.js"),
);
});
it("warns when a declared static asset is missing", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const warn = vi.fn();
copyStaticExtensionAssets({
rootDir,
assets: [{ src: "missing/file.mjs", dest: "dist/file.mjs" }],
warn,
});
expect(warn).toHaveBeenCalledWith(
"[runtime-postbuild] static asset not found, skipping: missing/file.mjs",
);
});
it("writes stable aliases for hashed root runtime modules", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-XyZ987.js"),
"export const auth = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-tts.runtime-AbCd1234.mjs"),
"export const tts = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "dispatch.contract-AbCd1234.mjs"),
"export const dispatch = true;\n",
"utf8",
);
for (const extension of ["js", "mjs"]) {
await fs.writeFile(
path.join(distDir, `library-Other123.${extension}`),
"export const x = true;\n",
"utf8",
);
}
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-model-auth.runtime.js"), "utf8")).toBe(
'export * from "./runtime-model-auth.runtime-XyZ987.js";\n',
);
expect(await fs.readFile(path.join(distDir, "runtime-tts.runtime.js"), "utf8")).toBe(
'export * from "./runtime-tts.runtime-AbCd1234.mjs";\n',
);
expect(await fs.readFile(path.join(distDir, "dispatch.contract.js"), "utf8")).toBe(
'export * from "./dispatch.contract-AbCd1234.mjs";\n',
);
for (const fileName of ["library.js", "runtime-tts.runtime.mjs", "dispatch.contract.mjs"]) {
await expectPathMissing(path.join(distDir, fileName));
}
});
it("refuses to rewrite stable aliases through a symlinked dist root", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-symlink-");
const targetDir = path.join(rootDir, "gateway-dist");
await fs.mkdir(targetDir, { recursive: true });
const hashedFile = path.join(targetDir, "runtime-model-auth.runtime-XyZ987.js");
await fs.writeFile(hashedFile, "export const auth = true;\n", "utf8");
const distLink = path.join(rootDir, "dist");
await fs.symlink(targetDir, distLink, "dir");
expect(() => writeStableRootRuntimeAliases({ rootDir })).toThrow(/symbolic link/u);
expect(await fs.readlink(distLink)).toBe(targetDir);
expect(await fs.readFile(hashedFile, "utf8")).toBe("export const auth = true;\n");
await expectPathMissing(path.join(targetDir, "runtime-model-auth.runtime.js"));
});
it("forwards default exports through stable and legacy aliases", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(path.join(rootDir, "package.json"), '{"type":"module"}\n', "utf8");
await fs.writeFile(
path.join(distDir, "text-transforms.runtime-Hash111.mjs"),
"function reconcile(value) { return value; }\nexport { reconcile as default };\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "mixed.contract-Hash222.mjs"),
"export const named = true;\nexport default function run() {}\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "named-only.runtime-Hash333.js"),
'const marker = "export { marker as default }";\nexport { marker };\n',
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
writeLegacyRootRuntimeCompatAliases({ rootDir });
const stable = await import(
pathToFileURL(path.join(distDir, "text-transforms.runtime.js")).href
);
const legacy = await import(
pathToFileURL(path.join(distDir, "text-transforms.runtime-sEqsN4pN.js")).href
);
const mixed = await import(pathToFileURL(path.join(distDir, "mixed.contract.js")).href);
const namedOnly = await import(pathToFileURL(path.join(distDir, "named-only.runtime.js")).href);
expect(stable.default("stable")).toBe("stable");
expect(legacy.default("legacy")).toBe("legacy");
expect(mixed.default).toBeTypeOf("function");
expect(mixed.named).toBe(true);
expect(namedOnly.marker).toBe("export { marker as default }");
expect(namedOnly).not.toHaveProperty("default");
rewriteRootRuntimeImportsToStableAliases({ rootDir });
writeStableRootRuntimeAliases({ rootDir });
writeLegacyRootRuntimeCompatAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "text-transforms.runtime.js"), "utf8")).toBe(
'export * from "./text-transforms.runtime-Hash111.mjs";\n' +
'export { default } from "./text-transforms.runtime-Hash111.mjs";\n',
);
const stableAfterRerun = await import(
`${pathToFileURL(path.join(distDir, "text-transforms.runtime.js")).href}?rerun=1`
);
expect(stableAfterRerun.default("rerun")).toBe("rerun");
});
it("does not write ambiguous stable aliases for colliding root runtime chunks", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.mjs"),
"export const pluginInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime.js"),
'export * from "./install.runtime-Stale.js";\n',
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
await expectPathMissing(path.join(distDir, "install.runtime.js"));
});
it("writes a stable plugin install runtime alias when install runtimes collide", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.mjs"),
[
"export const scanPackageInstallSource = true;",
"export const scanFileInstallSource = true;",
"export const scanInstalledPackageDependencyTree = true;",
"export const scanBundleInstallSource = true;",
"",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "install.runtime.js"), "utf8")).toBe(
'export * from "./install.runtime-Aaa111.mjs";\n',
);
});
it("keeps stable aliases when one colliding root runtime chunk re-exports the implementation", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-Impl123.js"),
"export const auth = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-Wrap456.mjs"),
'import { auth } from "./runtime-model-auth.runtime-Impl123.js";\nexport { auth };\n',
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-model-auth.runtime.js"), "utf8")).toBe(
'export * from "./runtime-model-auth.runtime-Wrap456.mjs";\n',
);
});
it("ignores legacy wrappers to the stable runtime alias when choosing the implementation", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-NewHash.mjs"),
"export const ready = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-OldHash.js"),
'export * from "./runtime-plugins.runtime.js";\n',
"utf8",
);
await fs.writeFile(
path.join(distDir, "dispatch-OldHash.js"),
['const lazy = () => import("./runtime-plugins.runtime-NewHash.mjs");', ""].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "dispatch-OldHash.js"), "utf8")).toBe(
['const lazy = () => import("./runtime-plugins.runtime.js");', ""].join("\n"),
);
expect(await fs.readFile(path.join(distDir, "runtime-plugins.runtime.js"), "utf8")).toBe(
'export * from "./runtime-plugins.runtime-NewHash.mjs";\n',
);
});
it.each(["js", "mjs"])("rewrites mixed runtime imports in a %s importer", async (extension) => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-AbCd1234.mjs"),
"export const ready = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "dispatch.contract-AbCd1234.js"),
"export const dispatch = true;\n",
"utf8",
);
const importerPath = path.join(distDir, `dispatch-OldHash.${extension}`);
await fs.writeFile(
importerPath,
[
'const lazy = () => import("./runtime-plugins.runtime-AbCd1234.mjs");',
'export { dispatch } from "./dispatch.contract-AbCd1234.js";',
'import "./missing.runtime-Nope.js";',
'import "./missing.contract-Nope.mjs";',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(importerPath, "utf8")).toBe(
[
'const lazy = () => import("./runtime-plugins.runtime.js");',
'export { dispatch } from "./dispatch.contract.js";',
'import "./missing.runtime-Nope.js";',
'import "./missing.contract-Nope.mjs";',
"",
].join("\n"),
);
});
it("keeps text-transform runtime imports hashed after the stable alias export surface grew", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "text-transforms.runtime-NewHash.mjs"),
"export const n = true;\nexport const t = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "provider-runtime-NewHash.mjs"),
[
'import { n as applyPluginTextReplacements } from "./text-transforms.runtime-NewHash.mjs";',
"export { applyPluginTextReplacements };",
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "provider-runtime-NewHash.mjs"), "utf8")).toBe(
[
'import { n as applyPluginTextReplacements } from "./text-transforms.runtime-NewHash.mjs";',
"export { applyPluginTextReplacements };",
"",
].join("\n"),
);
expect(await fs.readFile(path.join(distDir, "text-transforms.runtime.js"), "utf8")).toBe(
'export * from "./text-transforms.runtime-NewHash.mjs";\n',
);
});
it("rewrites gateway shutdown imports to stable runtime aliases", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "server-close.runtime-AbCd1234.js"),
"export const close = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "server.impl-OldHash.js"),
[
'const closeModule = () => import("./server-close.runtime-AbCd1234.js");',
'const ordinaryChunk = () => import("./server-close-OldHash.js");',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "server.impl-OldHash.js"), "utf8")).toBe(
[
'const closeModule = () => import("./server-close.runtime.js");',
'const ordinaryChunk = () => import("./server-close-OldHash.js");',
"",
].join("\n"),
);
});
it("rewrites reply-dispatch imports to the stable provider dispatcher runtime alias", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "provider-dispatcher.runtime-NewHash.js"),
'export * from "./provider-dispatcher-ImplHash.js";\n',
"utf8",
);
await fs.writeFile(
path.join(distDir, "reply-dispatch-runtime-OldHash.js"),
['const dispatcher = () => import("./provider-dispatcher.runtime-NewHash.js");', ""].join(
"\n",
),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "reply-dispatch-runtime-OldHash.js"), "utf8")).toBe(
['const dispatcher = () => import("./provider-dispatcher.runtime.js");', ""].join("\n"),
);
expect(await fs.readFile(path.join(distDir, "provider-dispatcher.runtime.js"), "utf8")).toBe(
'export * from "./provider-dispatcher.runtime-NewHash.js";\n',
);
});
it("keeps hashed imports when a stable runtime alias would collide", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.mjs"),
"export const pluginInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install-OldHash.js"),
[
'const pluginRuntime = () => import("./install.runtime-Aaa111.mjs");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "install-OldHash.js"), "utf8")).toBe(
[
'const pluginRuntime = () => import("./install.runtime-Aaa111.mjs");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
);
});
it("rewrites plugin install runtime imports to stable aliases when install runtimes collide", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "install.runtime-Aaa111.mjs"),
[
"export const scanPackageInstallSource = true;",
"export const scanFileInstallSource = true;",
"export const scanInstalledPackageDependencyTree = true;",
"export const scanBundleInstallSource = true;",
"",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(distDir, "install.runtime-Bbb222.js"),
"export const daemonInstall = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "install-OldHash.js"),
[
'const pluginRuntime = () => import("./install.runtime-Aaa111.mjs");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "install-OldHash.js"), "utf8")).toBe(
[
'const pluginRuntime = () => import("./install.runtime.js");',
'const daemonRuntime = () => import("./install.runtime-Bbb222.js");',
"",
].join("\n"),
);
});
it("leaves stable alias files pointing at their hashed runtime chunks", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime-AbCd1234.mjs"),
"export const ready = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-plugins.runtime.js"),
'export * from "./runtime-plugins.runtime-AbCd1234.mjs";\n',
"utf8",
);
rewriteRootRuntimeImportsToStableAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-plugins.runtime.js"), "utf8")).toBe(
'export * from "./runtime-plugins.runtime-AbCd1234.mjs";\n',
);
});
it("keeps every recorded previous-release lazy import loadable after replacement", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-update-compat-");
writeUpdateCompatibilityBuildFixture(rootDir);
runRuntimePostBuild({
rootDir,
env: { OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS: "0" },
timings: false,
});
for (const release of previousReleaseInventory.releases) {
for (const chunk of release.chunks) {
const loaded = await import(pathToFileURL(path.join(rootDir, "dist", chunk.path)).href);
for (const { exported, origin } of chunk.exports) {
expect(loaded[exported](), `${release.version}: ${chunk.path}:${exported}`).toBe(
origin.symbol,
);
}
}
}
});
it("keeps the 2026.9.1 Git updater restart import loadable after dist replacement", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-old-updater-");
const distDir = path.join(rootDir, "dist");
const ownerPath = path.join(distDir, "update-command-service-command.mjs");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
ownerPath,
'export async function restart() { return (await import("./shared-1Uyqkfns.js")).resolveNodeRunner(); }\n',
);
const output = childProcess.execFileSync(
testNodeExecPath,
[
"--import",
path.join(MODULE_ROOT, "scripts/tsx.mjs"),
"--input-type=module",
"-e",
[
'import fs from "node:fs/promises";',
'import path from "node:path";',
'import { pathToFileURL } from "node:url";',
`import { writeLegacyCliExitCompatChunks } from ${JSON.stringify(pathToFileURL(path.join(MODULE_ROOT, "scripts/runtime-postbuild.mts")).href)};`,
"const rootDir = process.argv[1];",
'const owner = await import(pathToFileURL(path.join(rootDir, "dist/update-command-service-command.mjs")).href);',
'await fs.rm(path.join(rootDir, "dist"), { recursive: true, force: true });',
"writeLegacyCliExitCompatChunks({ rootDir });",
"process.stdout.write(await owner.restart());",
].join("\n"),
rootDir,
],
{ encoding: "utf8" },
);
expect(output).toBe(testNodeExecPath);
});
it.each(["shared-Y6bNiw2w.js", "shared-DTaQo6Hi.js"])(
"preserves the old updater node-runner ABI through %s",
async (chunk) => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
writeLegacyCliExitCompatChunks({ rootDir });
const bridge = await import(pathToFileURL(path.join(rootDir, "dist", chunk)).href);
expect(bridge.resolveNodeRunner()).toBe(process.versions.bun ? "node" : process.execPath);
},
);
});
describe("previous release update compatibility", () => {
const integrity = `sha512-${Buffer.alloc(64).toString("base64")}`;
function write(root: string, relative: string, contents: string): void {
const file = path.join(root, relative);
fsSync.mkdirSync(path.dirname(file), { recursive: true });
fsSync.writeFileSync(file, contents);
}
function recordFixture(): UpdateCompatibilityInventory & {
releases: [UpdateCompatibilityRelease];
} {
const root = createTempDir("update-compat-release-");
write(root, "package.json", JSON.stringify({ name: "openclaw", version: "2026.9.1" }));
write(
root,
"dist/build-info.json",
JSON.stringify({ version: "2026.9.1", buildId: "fixture", commit: "0".repeat(40) }),
);
write(
root,
"dist/command.js",
[
"//#region src/cli/update-cli/update-command-service-command.ts",
'export async function restart() { return (await import("./service-abcdefgh.js")).runner(); }',
'export async function recover() { const { mode: selected } = await import("./service-abcdefgh.js"); return selected(); }',
].join("\n"),
);
write(
root,
"dist/service-abcdefgh.js",
'export { r as runner, m as mode } from "./implementation-12345678.js";',
);
write(
root,
"dist/implementation-12345678.js",
[
"//#region src/cli/update-cli/runner.ts",
'function resolveRunner() { return "old"; }',
"//#region src/cli/update-cli/recovery.ts",
'function resolveMode() { return "old"; }',
"export { resolveRunner as r, resolveMode as m };",
].join("\n"),
);
return {
schemaVersion: 1,
releases: [recordUpdateCompatibilityRelease({ packageDir: root, integrity })],
};
}
function candidate(root: string): void {
write(root, "src/cli/update-cli/recovery.ts", 'import { resolveMode } from "./mode.js";');
write(root, "src/cli/update-cli/mode.ts", 'export function resolveMode() { return "npm"; }');
write(
root,
"dist/current.mjs",
[
"//#region src/cli/update-cli/runner.ts",
'function resolveRunner() { return "node"; }',
"//#region src/cli/update-cli/mode.ts",
'function resolveMode() { return "npm"; }',
"export { resolveRunner as x, resolveMode as y };",
].join("\n"),
);
}
function recordImportedFixture(
expression: string,
modules: Record<string, string>,
identity: Pick<UpdateCompatibilityRelease, "version" | "buildId" | "commit" | "integrity"> = {
version: "2026.9.1",
buildId: "fixture",
commit: "0".repeat(40),
integrity,
},
owner = "src/cli/update-cli/update-command-service-command.ts",
) {
const root = createTempDir("update-compat-import-graph-");
write(
root,
"package.json",
JSON.stringify({ name: "openclaw", version: identity.version, type: "module" }),
);
write(root, "dist/build-info.json", JSON.stringify(identity));
write(
root,
"dist/command.js",
[`//#region ${owner}`, `export async function restart() { return ${expression}; }`].join(
"\n",
),
);
for (const [file, source] of Object.entries(modules)) {
write(root, `dist/${file}`, source);
}
const inventory: UpdateCompatibilityInventory = {
schemaVersion: 1,
releases: [
recordUpdateCompatibilityRelease({ packageDir: root, integrity: identity.integrity }),
],
};
return { root, inventory };
}
it.each(["generated", "changed delegation", "missing source region"])(
"traces only verified delegating config aliases (%s)",
(variant) => {
const facade =
'export { createConfigIO, readConfigFileSnapshot } from "./config-abcdefgh.mjs";\nexport * from "./extra.mjs";\n';
const alias = buildUpdateConfigRuntimeAlias(
"io.runtime-abcdefgh.mjs",
parser.parseSourceFile("facade.mjs", facade),
);
const record = () =>
recordImportedFixture('(await import("./io.runtime.js"))', {
"io.runtime.js":
variant === "changed delegation"
? alias.replace("return runtime[name]", "return undefined")
: alias,
"io.runtime-abcdefgh.mjs": facade,
"extra.mjs": "//#region src/config/extra.ts\nexport const targetOnly = true;\n",
"config-abcdefgh.mjs": [
'throw new Error("The recorder must not execute release code");',
...(variant === "missing source region" ? [] : ["//#region src/config/io.ts"]),
"export function createConfigIO() {}",
"export function readConfigFileSnapshot() {}",
].join("\n"),
});
if (variant !== "generated") {
expect(record).toThrow("Cannot trace io.runtime.js export createConfigIO");
return;
}
expect(record().inventory.releases[0]?.chunks).toMatchObject([
{
path: "io.runtime.js",
exports: ["createConfigIO", "readConfigFileSnapshot"].map((exported) => ({
exported,
origin: { module: "src/config/io.ts", symbol: exported },
})),
},
]);
},
);
it.each(["source scripts", "different owner", "mutable binding", "dist path", "unknown script"])(
"distinguishes source completion contracts from unknown dynamic imports (%s)",
(variant) => {
const declaration = variant === "mutable binding" ? "let" : "const";
const directory = variant === "dist path" ? "dist" : "scripts";
const script =
variant === "unknown script" ? "unknown.mts" : "stage-bundled-plugin-runtime.mts";
const expression = `await (async () => {
${declaration} stagingFile = path.join(root, "${directory}", "${script}");
await import(pathToFileURL(stagingFile).href);
await import(pathToFileURL(path.join(root, "scripts", "lib", "dist-artifact-ownership.mts")).href);
return (await import("./surface-abcdefgh.js")).x;
})()`;
const record = () =>
recordImportedFixture(
expression,
{
"surface-abcdefgh.js": "//#region src/infra/value.ts\nexport const x = 1;\n",
},
undefined,
variant === "different owner"
? undefined
: "src/cli/update-cli/update-command-runtime.ts",
);
if (variant !== "source scripts") {
expect(record).toThrow("Nonliteral post-swap import");
return;
}
expect(record().inventory.releases[0]?.chunks.map((chunk) => chunk.path)).toEqual([
"surface-abcdefgh.js",
]);
},
);
it.each([
{ name: "published bootstrap" },
{ name: "different owner", owner: "src/cli/update-cli/update-command-runtime.ts" },
{
name: "mutable root",
binding: "let driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });",
},
{ name: "unknown root", binding: "" },
{ name: "target root", binding: "const driverRoot = root;" },
{
name: "cwd root",
binding: "const driverRoot = resolveOpenClawPackageRootSync({ cwd: process.cwd() });",
},
{
name: "different module",
binding: "const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: targetUrl });",
},
{
name: "extra root options",
binding:
"const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url, cwd: root });",
},
{ name: "extra path", target: 'path.join(driverRoot, "node-runtime-recovery.mjs", "extra")' },
{ name: "dist path", target: 'path.join(driverRoot, "dist", "node-runtime-recovery.mjs")' },
{ name: "traversal", target: 'path.join(driverRoot, "../node-runtime-recovery.mjs")' },
{ name: "dynamic path", target: "path.join(driverRoot, entry)" },
{
name: "different URL form",
url: 'new URL("node-runtime-recovery.mjs", import.meta.url).href',
},
{ name: "import options", suffix: ", { with: options }" },
{ name: "for-of shadow", prefix: "for (const driverRoot of roots) ", declaration: "" },
{
name: "for initializer shadow",
prefix: "for (const driverRoot = root; driverRoot;) ",
declaration: "",
},
{ name: "arrow shadow", prefix: "const load = async (driverRoot) => ", declaration: "" },
])(
"records dist edges while qualifying the package bootstrap ($name)",
({
name,
owner = "src/cli/update-cli/update-command-node-runtime-resolution.ts",
binding = "const driverRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });",
target = 'path.join(driverRoot, "node-runtime-recovery.mjs")',
url = `pathToFileURL(${target}).href`,
suffix = "",
prefix = "",
declaration = "const { findUsableNodeRuntime } = ",
}) => {
const expression = `await (async () => {
${binding}
if (!driverRoot) return;
${prefix}${declaration}await import(${url}${suffix});
return (await import("./surface-abcdefgh.js")).x;
})()`;
const record = () =>
recordImportedFixture(
expression,
{ "surface-abcdefgh.js": "//#region src/infra/value.ts\nexport const x = 1;\n" },
undefined,
owner,
);
if (name !== "published bootstrap") {
expect(record).toThrow("Nonliteral post-swap import");
return;
}
expect(record().inventory.releases[0]?.chunks.map((chunk) => chunk.path)).toEqual([
"surface-abcdefgh.js",
]);
},
);
it.each(
previousReleaseInventory.releases
.filter(({ version }) => ["2026.9.1", "2026.9.2", "2026.9.3", "2026.9.4"].includes(version))
.flatMap((release) =>
["exact", "version", "buildId", "commit", "integrity", "chunk", "owner", "symbol"].map(
(changed) => ({ release, changed }),
),
),
)(
"corrects only verified coalesced release provenance ($release.version, $changed)",
async ({ release, changed }) => {
const chunk = release.chunks.find((entry) =>
entry.exports.some((item) => item.exported === "markPluginRegistryRetired"),
);
if (!chunk) {
throw new Error(`Missing historical retirement import for ${release.version}`);
}
const identity = { ...release };
if (changed === "version") {
identity.version = "2026.9.99";
}
if (changed === "buildId") {
identity.buildId = "different-build";
}
if (changed === "commit") {
identity.commit = "0".repeat(40);
}
if (changed === "integrity") {
identity.integrity = integrity;
}
const target = changed === "chunk" ? "registry-lifecycle-unknown1.js" : chunk.path;
const module =
changed === "owner" ? "src/plugins/another-owner.ts" : "src/plugins/loader-cache-state.ts";
const symbol = changed === "symbol" ? "anotherRetirement" : "markPluginRegistryRetired";
const { inventory } = recordImportedFixture(
`(await import("./${target}")).${symbol}`,
{ [target]: `//#region ${module}\nfunction ${symbol}() {}\nexport { ${symbol} };\n` },
identity,
);
expect(inventory.releases[0]?.chunks[0]?.exports).toEqual([
{
exported: symbol,
origin: {
module: changed === "exact" ? "src/plugins/registry-lifecycle.ts" : module,
symbol,
},
},
]);
if (changed !== "exact") {
return;
}
const current = createTempDir("update-compat-corrected-origin-");
write(current, "package.json", '{"type":"module"}');
write(
current,
"src/plugins/registry-lifecycle.ts",
'export function markPluginRegistryRetired() { return "current"; }',
);
write(
current,
"dist/current.mjs",
'//#region src/plugins/registry-lifecycle.ts\nfunction markPluginRegistryRetired() { return "current"; }\nexport { markPluginRegistryRetired };\n',
);
writeUpdateCompatibilityChunks({
distDir: path.join(current, "dist"),
sourceDir: current,
inventory,
});
const bridge = await import(pathToFileURL(path.join(current, "dist", target)).href);
expect(bridge.markPluginRegistryRetired()).toBe("current");
},
);
it.each([
{
access: "then",
expression: 'import("./surface-abcdefgh.js").then((module) => module.x)',
names: ["x", "y"],
},
{
access: "catch",
expression: 'import("./surface-abcdefgh.js").catch(() => undefined)',
names: ["x", "y"],
},
{
access: "finally",
expression: 'import("./surface-abcdefgh.js").finally(() => undefined)',
names: ["x", "y"],
},
{
access: "awaited property",
expression: '(await import("./surface-abcdefgh.js")).x',
names: ["x"],
},
{
access: "awaited bracket",
expression: '(await import("./surface-abcdefgh.js"))["x"]',
names: ["x"],
},
])(
"records namespace exports instead of Promise methods for $access access",
({ expression, names }) => {
const { inventory } = recordImportedFixture(expression, {
"surface-abcdefgh.js":
"//#region src/infra/values.ts\nconst x = 1; const y = 2; export { x, y };\n",
});
expect(inventory.releases[0]?.chunks).toMatchObject([
{
path: "surface-abcdefgh.js",
imports: [{ exports: names }],
exports: names.map((exported) => ({
exported,
origin: { module: "src/infra/values.ts", symbol: exported },
})),
},
]);
},
);
it.each(["distinct owners", "the same source annotation"])(
"rejects conflicting emitted star bindings with %s and names both sources",
(annotation) => {
const leftOwner = annotation === "the same source annotation" ? "shared" : "left";
const rightOwner = annotation === "the same source annotation" ? "shared" : "right";
expect(() =>
recordImportedFixture('(await import("./surface-abcdefgh.js")).x', {
"surface-abcdefgh.js": 'export * from "./left.mjs"; export * from "./right.mjs";\n',
"left.mjs": `//#region src/infra/${leftOwner}.ts\nexport const x = "left";\n`,
"right.mjs": `//#region src/infra/${rightOwner}.ts\nexport const x = "right";\n`,
}),
).toThrow(/(?=[\s\S]*left\.mjs)(?=[\s\S]*right\.mjs)/);
},
);
it("resolves a star diamond to the single declaration shared by both paths", async () => {
const { root, inventory } = recordImportedFixture('(await import("./surface-abcdefgh.js")).x', {
"surface-abcdefgh.js": 'export * from "./left.mjs"; export * from "./right.mjs";\n',
"left.mjs": 'export { x } from "./origin.mjs";\n',
"right.mjs": 'export * from "./origin.mjs";\n',
"origin.mjs": '//#region src/infra/origin.ts\nexport const x = { value: "shared" };\n',
});
const namespace = await import(pathToFileURL(path.join(root, "dist/surface-abcdefgh.js")).href);
const declaration = await import(pathToFileURL(path.join(root, "dist/origin.mjs")).href);
expect(namespace.x).toBe(declaration.x);
expect(inventory.releases[0]?.chunks[0]?.exports).toEqual([
{ exported: "x", origin: { module: "src/infra/origin.ts", symbol: "x" } },
]);
});
it("lets an explicit export resolve an otherwise ambiguous star name", async () => {
const { root, inventory } = recordImportedFixture('(await import("./surface-abcdefgh.js")).x', {
"surface-abcdefgh.js":
'export * from "./left.mjs"; export * from "./right.mjs"; export { x } from "./left.mjs";\n',
"left.mjs": '//#region src/infra/left.ts\nexport const x = "left";\n',
"right.mjs": '//#region src/infra/right.ts\nexport const x = "right";\n',
});
const namespace = await import(pathToFileURL(path.join(root, "dist/surface-abcdefgh.js")).href);
expect(namespace.x).toBe("left");
expect(inventory.releases[0]?.chunks[0]?.exports).toEqual([
{ exported: "x", origin: { module: "src/infra/left.ts", symbol: "x" } },
]);
});
it.each(["surface.js", "surface-abcdefgh.js"])(
"rejects an existing %s whose star ambiguity omits the required export",
(target) => {
const { inventory } = recordImportedFixture(`(await import("./${target}")).x`, {
[target]: '//#region src/infra/original.ts\nexport const x = "original";\n',
});
const root = createTempDir("update-compat-existing-stars-");
write(root, "package.json", '{"type":"module"}\n');
write(root, `dist/${target}`, 'export * from "./left.mjs"; export * from "./right.mjs";\n');
write(root, "dist/left.mjs", '//#region src/infra/original.ts\nexport const x = "left";\n');
write(root, "dist/right.mjs", '//#region src/infra/other.ts\nexport const x = "right";\n');
const namespaceHasX = childProcess.execFileSync(
testNodeExecPath,
[
"--input-type=module",
"-e",
'const namespace = await import(process.argv[1]); console.log(Object.hasOwn(namespace, "x"));',
pathToFileURL(path.join(root, "dist", target)).href,
],
{ encoding: "utf8" },
);
expect(namespaceHasX.trim()).toBe("false");
expect(() =>
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
}),
).toThrow(`${target} lacks x`);
},
);
it.each([
{
route: "source star forwarding",
owner: 'export * from "./moved.js";\n',
module: "src/infra/moved.ts",
symbol: "oldName",
},
{
route: "a local export alias",
owner: 'function newName() { return "current"; } export { newName as oldName };\n',
module: "src/infra/old.ts",
symbol: "newName",
},
])("bridges a moved declaration through $route", async ({ owner, module, symbol }) => {
const { inventory } = recordImportedFixture('(await import("./surface-abcdefgh.js")).x', {
"surface-abcdefgh.js":
'//#region src/infra/old.ts\nfunction oldName() { return "old"; } export { oldName as x };\n',
});
const root = createTempDir("update-compat-source-forwarding-");
write(root, "package.json", '{"type":"module"}\n');
write(root, "src/infra/old.ts", owner);
if (module !== "src/infra/old.ts") {
write(root, module, 'export function oldName() { return "current"; }\n');
}
write(
root,
"dist/current.mjs",
`//#region ${module}\nfunction ${symbol}() { return "current"; } export { ${symbol} as n };\n`,
);
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
});
const bridge = await import(pathToFileURL(path.join(root, "dist/surface-abcdefgh.js")).href);
expect(bridge.x()).toBe("current");
});
it("refuses ambiguous source stars and names both possible declaration owners", () => {
const { inventory } = recordImportedFixture('(await import("./surface-abcdefgh.js")).x', {
"surface-abcdefgh.js":
'//#region src/infra/old.ts\nfunction oldName() { return "old"; } export { oldName as x };\n',
});
const root = createTempDir("update-compat-source-conflict-");
write(root, "src/infra/old.ts", 'export * from "./left.js"; export * from "./right.js";\n');
for (const side of ["left", "right"]) {
write(root, `src/infra/${side}.ts`, `export function oldName() { return "${side}"; }\n`);
write(
root,
`dist/${side}.mjs`,
`//#region src/infra/${side}.ts\nfunction oldName() { return "${side}"; } export { oldName as n };\n`,
);
}
expect(() =>
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
}),
).toThrow(/(?=[\s\S]*left\.ts)(?=[\s\S]*right\.ts)/);
expect(fsSync.existsSync(path.join(root, "dist/surface-abcdefgh.js"))).toBe(false);
});
function writeWindowInventory(root: string): string {
const release = recordFixture().releases[0];
const output = path.join(root, "inventory.json");
write(
root,
"inventory.json",
JSON.stringify({
schemaVersion: 1,
releases: [
release,
{ ...release, version: "2026.9.2", chunks: [] },
{ ...release, version: "2026.9.3", chunks: [] },
],
}),
);
return output;
}
function runInventoryCli(
args: string[],
replies: Array<{ args: string[]; value: unknown }> = [],
) {
const root = createTempDir("update-compat-npm-");
const bin = path.join(root, "bin");
const callsFile = path.join(root, "calls.json");
const stub = path.join(bin, "npm.cjs");
write(
root,
"bin/npm.cjs",
[
`#!${testNodeExecPath}`,
'const fs = require("node:fs");',
`const callsFile = ${JSON.stringify(callsFile)};`,
`const replies = ${JSON.stringify(replies)};`,
'const calls = fs.existsSync(callsFile) ? JSON.parse(fs.readFileSync(callsFile, "utf8")) : [];',
"const args = process.argv.slice(2);",
"const reply = replies[calls.length];",
"calls.push(args);",
"fs.writeFileSync(callsFile, JSON.stringify(calls));",
'if (!reply || JSON.stringify(reply.args) !== JSON.stringify(args)) throw new Error("unexpected npm call: " + JSON.stringify(args));',
"console.log(JSON.stringify(reply.value));",
].join("\n"),
);
if (process.platform === "win32") {
write(root, "bin/npm.cmd", `@"${testNodeExecPath}" "${stub}" %*\r\n`);
} else {
fsSync.copyFileSync(stub, path.join(bin, "npm"));
fsSync.chmodSync(path.join(bin, "npm"), 0o755);
}
const result = childProcess.spawnSync(
testNodeExecPath,
[path.join(MODULE_ROOT, "scripts/update-compat-inventory.mts"), ...args],
{
encoding: "utf8",
env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}` },
timeout: 30_000,
},
);
const calls: unknown = fsSync.existsSync(callsFile)
? JSON.parse(fsSync.readFileSync(callsFile, "utf8"))
: [];
return { result, calls };
}
it("records an ordered supported window, including releases without post-swap imports, offline", () => {
const root = createTempDir("update-compat-window-");
const output = path.join(root, "inventory.json");
const args = ["--output", output];
for (const version of ["2026.9.3", "2026.9.1", "2026.9.2"]) {
const packageDir = path.join(root, version);
write(packageDir, "package.json", JSON.stringify({ name: "openclaw", version }));
write(
packageDir,
"dist/build-info.json",
JSON.stringify({ version, buildId: version, commit: "0".repeat(40) }),
);
write(packageDir, "dist/entry.js", "export {};\n");
args.push("--release", `${packageDir}=${integrity}`);
}
const generated = runInventoryCli(args);
expect(generated.result.status, generated.result.stderr).toBe(0);
expect(generated.calls).toEqual([]);
expect(
readUpdateCompatibilityInventory(output).releases.map(({ version, chunks }) => ({
version,
chunks,
})),
).toEqual([
{ version: "2026.9.1", chunks: [] },
{ version: "2026.9.2", chunks: [] },
{ version: "2026.9.3", chunks: [] },
]);
const checked = runInventoryCli([...args, "--check"]);
expect(checked.result.status, checked.result.stderr).toBe(0);
expect(checked.calls).toEqual([]);
});
it.each(["npm 11", "npm 12"])(
"checks %s latest and beta coverage without refetching recorded integrity",
(npmVersion) => {
const output = writeWindowInventory(createTempDir("update-compat-tag-coverage-"));
const tags = { latest: "2026.9.3", beta: "2026.9.2" };
const npmArgs = ["view", "openclaw", "dist-tags", "--json"];
const { result, calls } = runInventoryCli(
["--check", "--output", output],
[{ args: npmArgs, value: npmVersion === "npm 12" ? [tags] : tags }],
);
expect(result.status, result.stderr).toBe(0);
expect(calls).toEqual([npmArgs]);
},
);
it.each([
{ latest: "2026.9.3", beta: "2026.9.4-beta.1", missing: "2026.9.4-beta.1" },
{ latest: "2026.9.4", beta: "2026.9.4", missing: "2026.9.4" },
])(
"prints a complete refresh command when $missing leaves the window",
({ latest, beta, missing }) => {
const output = writeWindowInventory(createTempDir("update-compat-tag-refresh-"));
const newIntegrity = `sha512-${Buffer.alloc(64, 1).toString("base64")}`;
const tags = { latest, beta };
const expectedCalls = [
["view", "openclaw", "dist-tags", "--json"],
["view", `openclaw@${missing}`, "dist.integrity", "--json"],
];
const { result, calls } = runInventoryCli(
["--check", "--output", output],
[
{ args: expectedCalls[0]!, value: latest === beta ? [tags] : tags },
{ args: expectedCalls[1]!, value: latest === beta ? [newIntegrity] : newIntegrity },
],
);
expect(result.status).toBe(1);
expect(result.stderr).toContain("pnpm update:compat:gen --output");
expect(result.stderr).toContain(output);
expect(result.stderr).toContain(
[
...["2026.9.1", "2026.9.2", "2026.9.3"].map(
(version) => `--release .artifacts/update-compat/${version}/package=${integrity}`,
),
`--release .artifacts/update-compat/${missing}/package=${newIntegrity}`,
].join(" "),
);
expect(calls).toEqual(expectedCalls);
},
);
it.each([
{ tags: { beta: "2026.9.3" }, invalid: "latest" },
{ tags: { latest: "2026.9.3", beta: "not-a-release" }, invalid: "beta" },
])("refuses an absent or invalid $invalid dist-tag", ({ tags, invalid }) => {
const output = writeWindowInventory(createTempDir("update-compat-invalid-tags-"));
const npmArgs = ["view", "openclaw", "dist-tags", "--json"];
const { result, calls } = runInventoryCli(
["--check", "--output", output],
[{ args: npmArgs, value: tags }],
);
expect(result.status).toBe(1);
expect(result.stderr).toContain(`dist-tag ${invalid} is missing or invalid`);
expect(calls).toEqual([npmArgs]);
});
it("refuses conflicting release origins before writing any bridge", () => {
const inventory = recordFixture();
const newer = structuredClone(inventory.releases[0]);
newer.version = "2026.9.2";
const firstExport = newer.chunks[0]?.exports[0];
if (!firstExport) {
throw new Error("Fixture has no recorded export");
}
firstExport.origin.module = "src/cli/update-cli/different.ts";
inventory.releases.push(newer);
const conflict =
/Release inventories disagree about service-abcdefgh.js:mode: 2026.9.1 uses .*; 2026.9.2 uses/;
expect(() => parseUpdateCompatibilityInventory(inventory)).toThrow(conflict);
const root = createTempDir("update-compat-conflict-");
candidate(root);
expect(() =>
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
}),
).toThrow(conflict);
expect(fsSync.existsSync(path.join(root, "dist/service-abcdefgh.js"))).toBe(false);
});
it("keeps compatibility facades out of current runtime alias selection on rebuild", async () => {
const inventory = recordFixture();
inventory.releases[0].chunks = inventory.releases[0].chunks.map((chunk) => ({
...chunk,
path: "worker.runtime-12345678.js",
}));
const root = createTempDir("update-compat-rebuild-");
candidate(root);
fsSync.renameSync(
path.join(root, "dist/current.mjs"),
path.join(root, "dist/worker.runtime-abcdefgh.mjs"),
);
writeStableRootRuntimeAliases({ rootDir: root });
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
});
writeStableRootRuntimeAliases({ rootDir: root });
const current = await import(pathToFileURL(path.join(root, "dist/worker.runtime.js")).href);
expect(current.x()).toBe("node");
expect(current.y()).toBe("npm");
});
it("records emitted aliases and forwards old consumers to the current implementations", async () => {
const inventory = recordFixture();
expect(inventory.releases[0].chunks).toMatchObject([
{
path: "service-abcdefgh.js",
exports: [
{
exported: "mode",
origin: { module: "src/cli/update-cli/recovery.ts", symbol: "resolveMode" },
},
{
exported: "runner",
origin: { module: "src/cli/update-cli/runner.ts", symbol: "resolveRunner" },
},
],
},
]);
const root = createTempDir("update-compat-candidate-");
candidate(root);
const options = { distDir: path.join(root, "dist"), sourceDir: root, inventory };
writeUpdateCompatibilityChunks(options);
const bridge = path.join(root, "dist/service-abcdefgh.js");
const first = fsSync.readFileSync(bridge, "utf8");
writeUpdateCompatibilityChunks(options);
expect(fsSync.readFileSync(bridge, "utf8")).toBe(first);
const loaded = await import(pathToFileURL(bridge).href);
expect(loaded.runner()).toBe("node");
expect(loaded.mode()).toBe("npm");
});
it.each([
'import { y as mode } from "../current.mjs"; export { mode as forwarded };',
'export { y as forwarded } from "../current.mjs";',
'export * from "../current.mjs";',
])("bridges shared declaration bindings through %s", async (forwarding) => {
const inventory = recordFixture();
const root = createTempDir("update-compat-shared-binding-");
candidate(root);
fsSync.appendFileSync(path.join(root, "dist/current.mjs"), "\nexport { resolveMode as z };\n");
write(
root,
"dist/a-facade/forward.mjs",
`//#region src/cli/update-cli/mode.ts\n${forwarding}\n`,
);
const options = { distDir: path.join(root, "dist"), sourceDir: root, inventory };
writeUpdateCompatibilityChunks(options);
const bridge = path.join(root, "dist/service-abcdefgh.js");
const contents = fsSync.readFileSync(bridge, "utf8");
expect(contents).toContain('export { y as mode } from "./current.mjs";');
writeUpdateCompatibilityChunks(options);
expect(fsSync.readFileSync(bridge, "utf8")).toBe(contents);
const loaded = await import(pathToFileURL(bridge).href);
const current = await import(pathToFileURL(path.join(root, "dist/current.mjs")).href);
expect(loaded.mode).toBe(current.y);
expect(loaded.mode()).toBe("npm");
expect(loaded.runner).toBe(current.x);
});
it.each([
["config-doctor", "present"],
["config-doctor", "missing"],
["native-hook-relay", "present"],
["native-hook-relay", "missing"],
])(
"excludes the isolated %s graph when the runtime binding is %s",
async (directory, runtime) => {
const inventory = recordFixture();
const root = createTempDir("update-compat-isolated-graph-");
candidate(root);
const current = path.join(root, "dist/current.mjs");
write(root, `dist/${directory}/inspect.mjs`, fsSync.readFileSync(current, "utf8"));
const options = { distDir: path.join(root, "dist"), sourceDir: root, inventory };
if (runtime === "missing") {
fsSync.unlinkSync(current);
expect(() => writeUpdateCompatibilityChunks(options)).toThrow(
/no equivalent current export/,
);
expect(fsSync.existsSync(path.join(root, "dist/service-abcdefgh.js"))).toBe(false);
return;
}
writeUpdateCompatibilityChunks(options);
const bridge = await import(pathToFileURL(path.join(root, "dist/service-abcdefgh.js")).href);
const declaration = await import(pathToFileURL(current).href);
expect(bridge.mode).toBe(declaration.y);
expect(bridge.runner).toBe(declaration.x);
},
);
it.each(["missing", "ambiguous"])(
"refuses %s required implementations before writing bridges",
(failure) => {
const inventory = recordFixture();
const root = createTempDir("update-compat-refusal-");
candidate(root);
if (failure === "missing") {
write(root, "dist/current.mjs", "export {};\n");
} else {
fsSync.copyFileSync(
path.join(root, "dist/current.mjs"),
path.join(root, "dist/duplicate.mjs"),
);
}
expect(() =>
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
}),
).toThrow(/Cannot bridge service-abcdefgh\.js export mode/);
expect(fsSync.existsSync(path.join(root, "dist/service-abcdefgh.js"))).toBe(false);
},
);
it("does not replace a public entrypoint to conceal a missing export", () => {
const inventory = recordFixture();
inventory.releases[0].chunks = inventory.releases[0].chunks.map((chunk) => ({
...chunk,
path: "service.js",
}));
const root = createTempDir("update-compat-public-");
candidate(root);
const original = "export const other = true;\n";
write(root, "dist/service.js", original);
expect(() =>
writeUpdateCompatibilityChunks({
distDir: path.join(root, "dist"),
sourceDir: root,
inventory,
}),
).toThrow(/service\.js lacks mode/);
expect(fsSync.readFileSync(path.join(root, "dist/service.js"), "utf8")).toBe(original);
});
});