openclaw/test/scripts/tsdown-config.test.ts

1283 lines
52 KiB
TypeScript

// Tsdown config tests protect package artifact build contracts.
import { execFile } from "node:child_process";
import fs from "node:fs";
import { createRequire, isBuiltin } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "tsdown";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
collectRootPackageExcludedExtensionDirs,
DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV,
} from "../../scripts/lib/bundled-plugin-build-entries.mjs";
import {
collectPackageDistImportErrors,
collectPackageDistImports,
} from "../../scripts/lib/package-dist-imports.mjs";
import { publicPluginSdkEntrypoints } from "../../scripts/lib/plugin-sdk-entries.mts";
import {
TSDOWN_PACKAGE_CONFIG_GROUP,
TSDOWN_UNIFIED_CONFIG_GROUP,
TSDOWN_UNIFIED_DTS_CONFIG_GROUPS,
} from "../../scripts/lib/tsdown-config-groups.mts";
import { WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID } from "../../scripts/lib/worker-deploy-build-plugin.mts";
import { importFreshModule } from "../../src/plugin-sdk/test-helpers/import-fresh.js";
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
import buildConfigs from "../../tsdown.config.ts";
import { copyFsSafePackageFixture } from "./fs-safe-package.test-support.js";
import { materializeNativeCompiler } from "./native-boundary-fixture.js";
import { createScriptTestHarness } from "./test-helpers.js";
const configs = Array.isArray(buildConfigs) ? buildConfigs : [buildConfigs];
const { createTempDir } = createScriptTestHarness();
const testNodeExecPath = resolveTestNodeExecPath();
afterEach(() => vi.unstubAllEnvs());
type TsdownConfig = (typeof configs)[number];
type OutExtensions = NonNullable<TsdownConfig["outExtensions"]>;
function hasWorkerEntry(config: TsdownConfig, name: string, source: string): boolean {
const entry = config.entry;
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
return false;
}
return (entry as Record<string, unknown>)[name] === source;
}
const workerBuildTargets = [
["worker", "worker/worker", "src/worker/worker-deploy-entry.ts"],
[
"file-tool-planning",
"worker/file-tool-planning.worker",
"src/worker/worker-deploy-file-tool-planning.ts",
],
[
"image-processor",
"worker/image-processor.worker",
"src/worker/worker-deploy-image-processor.ts",
],
["sqlite-store", "worker/sqlite-store.worker", "src/worker/worker-deploy-sqlite-store.ts"],
["receiver", "worker/workspace-rsync-receiver", "src/worker/workspace-rsync-receiver.ts"],
["github-launcher", "worker/github-exec-launcher", "src/agents/github-exec-launcher.ts"],
["service-relay", "worker/service-child-relay", "src/process/supervisor/service-child-relay.ts"],
[
"service-group-anchor",
"worker/service-child-group-anchor",
"src/process/supervisor/service-child-group-anchor.ts",
],
] as const;
const isWorkerBuildConfig = (config: TsdownConfig) =>
workerBuildTargets.some(([, entry, source]) => hasWorkerEntry(config, entry, source));
function findWorkerBuildConfig(target: string) {
const selected = workerBuildTargets.find(([name]) => name === target);
return selected && configs.find((config) => hasWorkerEntry(config, selected[1], selected[2]));
}
const FS_SAFE_CALLER_PROBE = `
import assert from "node:assert/strict";
import fs from "node:fs";
import { createRequire, isBuiltin, registerHooks } from "node:module";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [entry, observer, rootDir, mode, outcome, sealed] = process.argv.slice(1);
if (sealed) registerHooks({ resolve(specifier, context, next) {
if (!isBuiltin(specifier) && specifier !== pathToFileURL(entry).href)
throw new Error("sealed dependency escaped: " + specifier);
return next(specifier, context);
}});
const { root, parseJsonWithJson5Fallback, resolvePreferredOpenClawTmpDir, resolveRuntimeProcessEntrypointUrl } = await import(pathToFileURL(entry).href);
if (sealed) {
assert.deepEqual(parseJsonWithJson5Fallback("{value:'bundled',}"), {value:"bundled"});
assert.equal(resolvePreferredOpenClawTmpDir({preferredDir:rootDir, tmpdir:()=>rootDir, platform:"linux"}), rootDir);
assert.equal(resolveRuntimeProcessEntrypointUrl("githubExec").href, new URL("./github-exec-launcher.mjs", pathToFileURL(entry)).href);
assert.equal(resolveRuntimeProcessEntrypointUrl("serviceChildRelay").href, new URL("./service-child-relay.mjs", pathToFileURL(entry)).href);
}
const { configureFsSafeNative, getFsSafeNativeConfig, FsSafeError } = await import(pathToFileURL(observer).href);
assert.equal(getFsSafeNativeConfig().mode, mode === "configured" ? "off" : mode);
if (mode === "configured") configureFsSafeNative({ mode: "require" });
const scoped = await root(rootDir);
if (outcome === "missing") {
await assert.rejects(scoped.write("proof.txt", "native proof"), (error) => {
assert(error instanceof FsSafeError);
assert.equal(error.code, "helper-unavailable");
assert.equal(error.cause?.code, "MODULE_NOT_FOUND");
return true;
});
assert.deepEqual(fs.readdirSync(rootDir), []);
} else {
await scoped.write("proof.txt", "native proof");
await scoped.create("created.txt", "create proof");
assert.equal(fs.readFileSync(path.join(rootDir, "proof.txt"), "utf8"), "native proof");
assert.equal(fs.readFileSync(path.join(rootDir, "created.txt"), "utf8"), "create proof");
}
const loaded = Object.keys(createRequire(import.meta.url).cache).filter((file) => file.endsWith("fs-safe-native.node"));
assert.equal(loaded.length, outcome === "native" ? 1 : 0);
if (loaded.length) assert(loaded[0].startsWith(path.dirname(rootDir) + path.sep));
`;
describe("tsdown config", () => {
it.each([
"extensions/openai/setup-api",
"extensions/openai/capability-catalog",
"extensions/anthropic/provider-discovery",
])(
"keeps %s inventory chunks local and shares the host SDK across lazy imports",
async (entryName) => {
const selected = configs.find((config) =>
hasWorkerEntry(config, entryName, `${entryName}.ts`),
);
expect(selected).toBeDefined();
const root = fs.realpathSync(createTempDir("openclaw-tsdown-setup-"));
const sdkSpecifier = "openclaw/plugin-sdk/ssrf-runtime-internal";
fs.writeFileSync(path.join(root, "package.json"), '{"type":"module"}');
for (const [name, exports, source] of [
[
"openclaw",
{ "./plugin-sdk/ssrf-runtime-internal": "./index.js" },
"export const identity = Symbol();",
],
[
"setup-private-dependency",
{ ".": "./index.js" },
'export const value = "bundled plugin dependency";',
],
] as const) {
const packageRoot = path.join(root, "node_modules", name);
fs.mkdirSync(packageRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name, type: "module", exports }),
);
fs.writeFileSync(path.join(packageRoot, "index.js"), source);
}
const entry = path.join(root, "setup-api.ts");
fs.writeFileSync(
entry,
[
`import { identity } from ${JSON.stringify(sdkSpecifier)};`,
'export async function probe() { const lazy = await import("./provider.ts"); return [identity, lazy.identity, lazy.value]; }',
].join("\n"),
);
fs.writeFileSync(
path.join(root, "provider.ts"),
[
`export { identity } from ${JSON.stringify(sdkSpecifier)};`,
'export { value } from "setup-private-dependency";',
].join("\n"),
);
const { bundles } = await build({
...selected,
config: false,
cwd: root,
entry: { [entryName]: entry },
outDir: path.join(root, "dist"),
tsconfig: false,
dts: false,
logLevel: "silent",
});
try {
const chunks = bundles.flatMap((bundle) =>
bundle.chunks.filter((chunk) => chunk.type === "chunk"),
);
expect(chunks.flatMap((chunk) => chunk.imports)).toContain(sdkSpecifier);
const privateChunks = chunks.filter((chunk) => !chunk.isEntry);
expect(privateChunks.length).toBeGreaterThan(0);
expect(
privateChunks.every((chunk) =>
chunk.fileName.startsWith(`${path.dirname(entryName)}/.setup/`),
),
).toBe(true);
fs.rmSync(path.join(root, "node_modules/setup-private-dependency"), { recursive: true });
const script = `
import assert from "node:assert/strict";
import { identity } from ${JSON.stringify(sdkSpecifier)};
import { probe } from "./dist/${entryName}.js";
const [direct, lazy, value] = await probe();
assert.equal(direct, identity);
assert.equal(lazy, identity);
assert.equal(value, "bundled plugin dependency");
`;
const result = await new Promise<{ error: Error | null; stderr: string }>((resolve) => {
execFile(
testNodeExecPath,
["--input-type=module", "-e", script],
{ cwd: root, timeout: 30_000 },
(error, _stdout, stderr) => resolve({ error, stderr }),
);
});
expect(result.error, result.stderr).toBeNull();
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
},
);
it.each([false, true])(
"runs the Docker-selected memory store with only production dependencies (verbose=%s)",
async (verbose) => {
vi.stubEnv("OPENCLAW_BUILD_VERBOSE", verbose ? "1" : "0");
const entryName = "extensions/memory-lancedb/lancedb-store";
const runtimeEntryName = "extensions/memory-lancedb/lancedb-runtime";
const defaultConfig = configs.find((config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP);
expect(defaultConfig?.entry).not.toHaveProperty(entryName);
vi.stubEnv(DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV, "memory-lancedb");
// Selection is captured during config evaluation; keep the default graph untouched.
const { default: selectedConfigs } = await importFreshModule<
typeof import("../../tsdown.config.ts")
>(import.meta.url, `../../tsdown.config.ts?docker-memory-lancedb=${verbose}`);
const selected = selectedConfigs.find(
(config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP,
);
const source = (selected?.entry as Record<string, string> | undefined)?.[entryName];
const runtimeSource = (selected?.entry as Record<string, string> | undefined)?.[
runtimeEntryName
];
expect(source).toBeDefined();
expect(runtimeSource).toBeDefined();
const root = fs.realpathSync(createTempDir("openclaw-tsdown-memory-"));
const manifest = JSON.parse(
fs.readFileSync("extensions/memory-lancedb/package.json", "utf8"),
) as {
dependencies: Record<string, string>;
optionalDependencies: Record<string, string>;
};
fs.writeFileSync(path.join(root, "package.json"), '{"type":"module"}');
for (const name of Object.keys({
...manifest.dependencies,
...manifest.optionalDependencies,
})) {
const installed = path.resolve("extensions/memory-lancedb/node_modules", name);
if (!fs.existsSync(installed)) {
continue;
}
const destination = path.join(root, "node_modules", name);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.symlinkSync(fs.realpathSync(installed), destination, "dir");
}
const { bundles } = await build({
...selected,
config: false,
entry: { [entryName]: source!, [runtimeEntryName]: runtimeSource! },
outDir: path.join(root, "dist"),
dts: false,
logLevel: "silent",
});
try {
const script = `
import assert from "node:assert/strict";
import { registerHooks } from "node:module";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [root, entry, runtimeEntry, bindingsJson] = process.argv.slice(1);
const bindings = new Set(JSON.parse(bindingsJson));
const loadedBindings = new Set();
registerHooks({ resolve(specifier, context, nextResolve) {
assert(!["@lancedb/lancedb", "@huggingface/transformers", "sharp"].includes(specifier),
"Unexpected runtime dependency: " + specifier);
if (bindings.has(specifier)) loadedBindings.add(specifier);
return nextResolve(specifier, context);
} });
const { MemoryDB } = await import(pathToFileURL(entry).href);
const dbPath = path.join(root, "memory-db");
let db = new MemoryDB(dbPath, 2);
try {
const stored = await db.store("alpha", {
text: "Bundled memory proof", vector: [1, 0], importance: 0.8, category: "fact"
});
assert.equal((await db.search("alpha", [1, 0], 1, 0))[0].entry.id, stored.id);
assert.deepEqual(await db.search("beta", [1, 0], 1, 0), []);
db.close();
db = new MemoryDB(dbPath, 2);
assert.equal((await db.search("alpha", [1, 0], 1, 0))[0].entry.id, stored.id);
const { loadLanceDbModule } = await import(pathToFileURL(runtimeEntry).href);
const connection = await (await loadLanceDbModule()).connect(dbPath);
const table = await connection.openTable("memories");
try {
assert.equal(await table.countRows("agentId = 'alpha'"), 1);
assert.equal(await table.countRows("agentId = 'beta'"), 0);
} finally {
table.close();
connection.close();
}
assert(loadedBindings.size > 0, "Expected a declared native binding");
} finally {
db.close();
}
console.log("bundled memory store persists and recalls without image dependencies");
`;
const result = await new Promise<{ error: Error | null; stdout: string; stderr: string }>(
(resolve) => {
execFile(
testNodeExecPath,
[
"--input-type=module",
"-e",
script,
root,
path.join(root, "dist", `${entryName}.js`),
path.join(root, "dist", `${runtimeEntryName}.js`),
JSON.stringify(Object.keys(manifest.optionalDependencies)),
],
{ cwd: root, timeout: 30_000 },
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
},
);
expect(result.error, result.stderr).toBeNull();
expect(result.stdout.trim()).toBe(
"bundled memory store persists and recalls without image dependencies",
);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
},
);
it("routes HTML-only mail and advances the cursor through packaged imap-watch", async () => {
const entryName = "extensions/imap/index";
const selected = configs.find((config) =>
hasWorkerEntry(config, entryName, "extensions/imap/index.ts"),
);
if (!selected) {
throw new Error("Missing IMAP build config");
}
const root = fs.realpathSync(createTempDir("openclaw-tsdown-imap-"));
fs.writeFileSync(path.join(root, "package.json"), '{"type":"module"}');
fs.symlinkSync(fs.realpathSync("node_modules"), path.join(root, "node_modules"), "dir");
const { bundles } = await build({
...selected,
config: false,
entry: {
[entryName]: "extensions/imap/index.ts",
"plugin-sdk/plugin-state-store-runtime": "src/plugin-sdk/plugin-state-store-runtime.ts",
},
outDir: path.join(root, "dist"),
dts: false,
logLevel: "silent",
});
try {
const result = await new Promise<{ error: Error | null; stdout: string; stderr: string }>(
(resolve) => {
execFile(
testNodeExecPath,
[
fileURLToPath(new URL("./imap-packaged-service.test-support.mjs", import.meta.url)),
root,
],
{
cwd: root,
env: { ...process.env, OPENCLAW_STATE_DIR: path.join(root, "state") },
timeout: 30_000,
},
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
},
);
expect(result.error, result.stderr || result.stdout).toBeNull();
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
});
it("keeps writable database and session lifecycle outside the archive worker bootstrap", async () => {
const workerEntry = "config/sessions/session-accessor.sqlite-archive.worker";
const selected = configs.find((config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP);
if (!selected) {
throw new Error("Missing session archive worker build config");
}
const entries = selected.entry as Record<string, string>;
// Include the parent store: shared chunks must not pull its lifecycle writes
// into the one-shot materialize/publish worker's static closure.
const { bundles } = await build({
...selected,
config: false,
entry: Object.fromEntries(
[workerEntry, "plugin-sdk/session-store-runtime"].map((name) => [name, entries[name]!]),
),
outDir: fs.realpathSync(createTempDir("openclaw-archive-worker-imports-")),
dts: false,
logLevel: "silent",
});
try {
const chunks = new Map(
bundles.flatMap((bundle) =>
bundle.chunks
.filter((chunk) => chunk.type === "chunk")
.map((chunk) => [chunk.fileName, chunk] as const),
),
);
const queue = [`${workerEntry}.js`];
const visited = new Set<string>();
const modules = new Set<string>();
for (const name of queue) {
if (visited.has(name)) {
continue;
}
visited.add(name);
const chunk = chunks.get(name);
if (!chunk) {
throw new Error(`Missing archive worker chunk: ${name}`);
}
for (const [id, module] of Object.entries(chunk.modules)) {
if (module.renderedLength > 0) {
modules.add(id.replaceAll("\\", "/"));
}
}
for (const specifier of chunk.imports) {
const target = specifier.startsWith(".")
? path.posix.normalize(path.posix.join(path.posix.dirname(name), specifier))
: specifier;
if (chunks.has(target)) {
queue.push(target);
}
}
}
expect(
[...modules].filter(
(id) =>
id.endsWith("/src/state/openclaw-agent-db.ts") ||
/\/session-accessor\.sqlite-(?:reclamation|lifecycle-state|entry-store|archive)\.ts$/u.test(
id,
),
),
).toEqual([]);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
});
it("builds retained config repairs without plugin runtime or state migration closures", async () => {
const selected = configs.find((config) => config.outDir === "dist/config-doctor");
expect(selected?.name).toBe(TSDOWN_UNIFIED_CONFIG_GROUP);
const entries = selected?.entry ?? {};
expect(Object.keys(entries)).toContain("discord");
const root = fs.realpathSync(createTempDir("openclaw-retained-config-doctors-"));
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ type: "module" }));
const { bundles } = await build({
...selected,
config: false,
outDir: root,
dts: false,
logLevel: "silent",
});
try {
const chunks = new Map(
bundles.flatMap((bundle) =>
bundle.chunks
.filter((chunk) => chunk.type === "chunk")
.map((chunk) => [chunk.fileName, chunk] as const),
),
);
const queue = Object.keys(entries).map((entry) => `${entry}.js`);
const visited = new Set<string>();
const dependencies = JSON.parse(fs.readFileSync("package.json", "utf8")).dependencies;
while (queue.length) {
const name = queue.pop()!;
if (visited.has(name)) {
continue;
}
visited.add(name);
const chunk = chunks.get(name);
if (!chunk) {
throw new Error(`Missing retained config chunk: ${name}`);
}
for (const specifier of chunk.imports) {
const target = specifier.startsWith(".")
? path.posix.normalize(path.posix.join(path.posix.dirname(name), specifier))
: specifier;
if (chunks.has(target)) {
queue.push(target);
continue;
}
if (isBuiltin(specifier)) {
continue;
}
const packageName = specifier
.split("/")
.slice(0, specifier.startsWith("@") ? 2 : 1)
.join("/");
expect(Object.hasOwn(dependencies, packageName), specifier).toBe(true);
const destination = path.join(root, "node_modules", packageName);
if (!fs.existsSync(destination)) {
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.symlinkSync(
fs.realpathSync(path.join("node_modules", packageName)),
destination,
"dir",
);
}
}
expect(chunk.code).not.toMatch(
/migrateLegacyState|openPluginStateKeyedStore|openChannelIngressQueue/u,
);
const renderedModules = Object.entries(chunk.modules)
.filter(([, module]) => module.renderedLength > 0)
.map(([module]) => module.replaceAll("\\", "/"));
expect(
renderedModules.filter((module) =>
/\/extensions\/[^/]+\/(?:doctor-contract-api|runtime|index|channel-entry|setup-entry)\.[cm]?[jt]s$/u.test(
module,
),
),
).toEqual([]);
}
const script = `
import assert from "node:assert/strict";
import path from "node:path";
import { pathToFileURL } from "node:url";
const [root, entriesJson] = process.argv.slice(1);
const modules = {};
for (const name of JSON.parse(entriesJson)) {
const mod = await import(pathToFileURL(path.join(root, name + ".js")).href);
assert.deepEqual(Object.keys(mod).sort(), name === "clickclack"
? ["normalizeCompatibilityConfig"]
: ["legacyConfigRules", "normalizeCompatibilityConfig"]);
modules[name] = mod;
}
const cfg = { channels: { discord: { dm: { enabled: true, policy: "allowlist", allowFrom: ["123"] }, accounts: { work: { dm: { policy: "disabled", allowFrom: ["456"] } } } } }, plugins: { allow: [] } };
const before = structuredClone(cfg);
const migrated = modules.discord.normalizeCompatibilityConfig({ cfg }).config;
assert.deepEqual(cfg, before);
assert.deepEqual(migrated.channels.discord.dm, { enabled: true });
assert.equal(migrated.channels.discord.dmPolicy, "allowlist");
assert.deepEqual(migrated.channels.discord.allowFrom, ["123"]);
assert.equal(migrated.channels.discord.accounts.work.dmPolicy, "disabled");
assert.deepEqual(migrated.channels.discord.accounts.work.allowFrom, ["456"]);
assert.deepEqual(migrated.plugins, { allow: [] });
for (const name of ["imessage", "msteams"]) {
const result = modules[name].normalizeCompatibilityConfig({ cfg: { channels: { [name]: { blockStreaming: false } } } });
assert.equal(result.config.channels[name].streaming.block.enabled, false);
assert.equal(Object.hasOwn(result.config.channels[name], "blockStreaming"), false);
}
console.log("retained config APIs migrate without plugin installation or capability grants");
`;
const result = await new Promise<{ error: Error | null; stdout: string; stderr: string }>(
(resolve) => {
execFile(
testNodeExecPath,
["--input-type=module", "-e", script, root, JSON.stringify(Object.keys(entries))],
{ cwd: root, timeout: 30_000 },
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
},
);
expect(result.error, result.stderr).toBeNull();
expect(result.stdout.trim()).toBe(
"retained config APIs migrate without plugin installation or capability grants",
);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
});
it.each([
{ target: "runtime", entry: "parser" },
{ target: "nested", entry: "extensions/fixture/.setup/parser" },
{ target: "worker", entry: "worker/parser" },
])("loads the Bash grammar from the relocated $target package", async ({ target, entry }) => {
const root = fs.realpathSync(createTempDir("openclaw-bash-parser-"));
const worker = target === "worker";
const selected = worker
? findWorkerBuildConfig("worker")
: configs.find((config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP);
expect(selected).toBeDefined();
const outDir = path.join(root, "build");
const { bundles } = await build({
...selected,
config: false,
entry: { [entry]: path.resolve("src/infra/command-explainer/tree-sitter-runtime.ts") },
outDir,
dts: false,
logLevel: "silent",
});
try {
const installed = path.join(root, "installed package");
fs.mkdirSync(installed);
fs.writeFileSync(path.join(installed, "package.json"), JSON.stringify({ type: "module" }));
fs.renameSync(outDir, path.join(installed, "dist"));
const require = createRequire(import.meta.url);
if (worker) {
expect(bundles.flatMap((bundle) => bundle.chunks.map((chunk) => chunk.fileName))).toEqual([
`${entry}.mjs`,
]);
} else {
// Install only the engine package. The grammar must come from emitted assets,
// even when the entrypoint is nested and the whole package has moved.
const engineRoot = path.dirname(require.resolve("web-tree-sitter"));
fs.cpSync(engineRoot, path.join(installed, "node_modules/web-tree-sitter"), {
recursive: true,
});
const grammarRoot = path.dirname(require.resolve("tree-sitter-bash/tree-sitter-bash.wasm"));
for (const [output, source] of [
["tree-sitter-bash.wasm", "tree-sitter-bash.wasm"],
["tree-sitter-bash.LICENSE", "LICENSE"],
] as const) {
expect(fs.readFileSync(path.join(installed, "dist", output))).toEqual(
fs.readFileSync(path.join(grammarRoot, source)),
);
}
}
const result = await new Promise<{ error: Error | null; stdout: string; stderr: string }>(
(resolve) => {
execFile(
testNodeExecPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
const entry = pathToFileURL(process.argv[1]);
assert.throws(() => createRequire(entry).resolve("tree-sitter-bash"), { code: "MODULE_NOT_FOUND" });
const { parseBashForCommandExplanation } = await import(entry.href);
const tree = await parseBashForCommandExplanation('printf "%s" "$(whoami)" | cat');
try {
assert.equal(tree.rootNode.hasError, false);
assert.deepEqual(tree.rootNode.descendantsOfType("command").map(node => node.childForFieldName("name").text), ["printf", "whoami", "cat"]);
} finally {
tree.delete();
}
console.log("relocated Bash parser works without native grammar package");
`,
path.join(installed, "dist", `${entry}.${worker ? "mjs" : "js"}`),
],
{ cwd: installed, timeout: 30_000 },
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
},
);
expect(result.error, result.stderr).toBeNull();
expect(result.stdout.trim()).toBe(
"relocated Bash parser works without native grammar package",
);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
});
it.each(["runtime", "worker"])(
"keeps service relay dependencies inside the emitted %s artifact closure",
async (target) => {
const root = fs.realpathSync(createTempDir("openclaw-tsdown-service-relay-"));
const worker = target === "worker";
const prefix = worker ? "worker" : "process/supervisor";
const extension = worker ? "mjs" : "js";
const relay = `${prefix}/service-child-relay`;
const anchor = `${prefix}/service-child-group-anchor`;
const selectedConfigs = worker
? [findWorkerBuildConfig("service-relay"), findWorkerBuildConfig("service-group-anchor")]
: [relay, anchor].map((entry) =>
configs.find((config) =>
hasWorkerEntry(config, entry, path.resolve(`src/${entry}.ts`)),
),
);
const files: string[] = [];
for (const selected of selectedConfigs) {
if (!selected) {
throw new Error(`Missing ${target} service relay build config`);
}
const { bundles } = await build({
...selected,
config: false,
entry: Object.fromEntries(
Object.entries(selected.entry ?? {}).filter(
([name]) => name === relay || name === anchor,
),
),
outDir: path.join(root, "dist"),
clean: false,
dts: false,
logLevel: "silent",
});
try {
files.push(
...bundles.flatMap((bundle) => bundle.chunks.map((chunk) => `dist/${chunk.fileName}`)),
);
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
}
expect(files).toEqual(
expect.arrayContaining([`dist/${relay}.${extension}`, `dist/${anchor}.${extension}`]),
);
const imports = collectPackageDistImports({
files,
readText: (file) => fs.readFileSync(path.join(root, file), "utf8"),
});
expect(collectPackageDistImportErrors({ files, imports })).toEqual([]);
const sealedAnchorEdge = {
importerPath: `dist/${relay}.${extension}`,
importedPath: `dist/${anchor}.mjs`,
};
if (worker) {
expect(imports).toContainEqual(sealedAnchorEdge);
} else {
// Each launch boots both helpers; shared chunks pull unrelated runtime
// exports into startup before the durable worker gate can open.
expect(files.toSorted()).toEqual(
[`dist/${relay}.${extension}`, `dist/${anchor}.${extension}`].toSorted(),
);
expect(imports).toEqual([]);
}
},
);
it.each(["runtime", "worker"])(
"preserves fs-safe package ownership and policy in relocated %s output",
async (target) => {
const temporaryRoot = fs.realpathSync(createTempDir("openclaw-tsdown-fs-safe-"));
const sourceRoot = path.join(temporaryRoot, "build");
const relocatedRoot = path.join(temporaryRoot, "relocated");
fs.mkdirSync(sourceRoot);
fs.writeFileSync(path.join(sourceRoot, "package.json"), '{"type":"module"}');
const worker = target === "worker";
const require = createRequire(import.meta.url);
const { nativePackages } = worker
? { nativePackages: [] }
: copyFsSafePackageFixture(sourceRoot);
if (!worker) {
expect(nativePackages.length).toBeGreaterThan(0);
}
const sdkSource = path.resolve("src/plugin-sdk/memory-core-host-engine-fs.ts");
const observerSource = path.join(sourceRoot, "observer.ts");
fs.writeFileSync(
observerSource,
[
...(worker
? [
`import ${JSON.stringify(path.resolve("src/worker/worker-deploy-runtime.ts"))};`,
`export { parseJsonWithJson5Fallback } from ${JSON.stringify(path.resolve("src/utils/parse-json-compat.ts"))};`,
`export { resolvePreferredOpenClawTmpDir } from ${JSON.stringify(path.resolve("src/infra/tmp-openclaw-dir.ts"))};`,
`export { resolveRuntimeProcessEntrypointUrl } from ${JSON.stringify(path.resolve("src/infra/runtime-process-url.ts"))};`,
]
: []),
`export { root } from ${JSON.stringify(sdkSource)};`,
`export { configureFsSafeNative, getFsSafeNativeConfig } from ${JSON.stringify(worker ? require.resolve("@openclaw/fs-safe/config") : "@openclaw/fs-safe/config")};`,
`export { FsSafeError } from ${JSON.stringify(worker ? require.resolve("@openclaw/fs-safe/errors") : "@openclaw/fs-safe/errors")};`,
].join("\n"),
);
const selected = worker
? findWorkerBuildConfig("worker")
: configs.find((config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP);
expect(selected).toBeDefined();
const { bundles } = await build({
...selected,
config: false,
entry: worker
? { "worker/worker": observerSource }
: { "plugin-sdk/memory-core-host-engine-fs": sdkSource, observer: observerSource },
outDir: path.join(sourceRoot, "output"),
dts: false,
logLevel: "silent",
});
try {
fs.renameSync(sourceRoot, relocatedRoot);
const entry = path.join(
relocatedRoot,
worker ? "output/worker/worker.mjs" : "output/plugin-sdk/memory-core-host-engine-fs.js",
);
const observer = worker ? entry : path.join(relocatedRoot, "output/observer.js");
const probe = async (
name: string,
mode: string,
outcome: string,
override: NodeJS.ProcessEnv = {},
) => {
const rootDir = path.join(relocatedRoot, name);
fs.mkdirSync(rootDir);
const result = await new Promise<{ error: Error | null; stdout: string; stderr: string }>(
(resolve) => {
execFile(
testNodeExecPath,
[
"--input-type=module",
"--eval",
FS_SAFE_CALLER_PROBE,
entry,
observer,
rootDir,
mode,
outcome,
worker ? "sealed" : "",
],
{
cwd: relocatedRoot,
encoding: "utf8",
timeout: 30_000,
env: {
PATH: process.env.PATH,
SystemRoot: process.env.SystemRoot,
WINDIR: process.env.WINDIR,
HOME: temporaryRoot,
USERPROFILE: temporaryRoot,
TMPDIR: temporaryRoot,
TMP: temporaryRoot,
TEMP: temporaryRoot,
...override,
},
},
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
},
);
expect(result.error, `${name}\n${result.stdout}\n${result.stderr}`).toBeNull();
};
// Join every caller before removing its dependency tree, even after a failure.
const join = async (probes: Promise<void>[]) => {
const results = await Promise.allSettled(probes);
const errors = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (errors.length) {
throw new AggregateError(errors, "fs-safe package probes failed");
}
};
if (worker) {
await join([
probe("default", "off", "fallback"),
...["FS_SAFE_NATIVE_MODE", "OPENCLAW_FS_SAFE_NATIVE_MODE"].map((key) =>
probe(key, "off", "fallback", { [key]: "require" }),
),
]);
} else {
await join([
...["FS_SAFE_NATIVE_MODE", "OPENCLAW_FS_SAFE_NATIVE_MODE"].map((key) =>
probe(key, "require", "native", { [key]: "require" }),
),
probe("shared-config", "configured", "native", { FS_SAFE_NATIVE_MODE: "off" }),
probe("default", "auto", "native"),
]);
for (const nativePackage of nativePackages) {
fs.rmSync(path.join(relocatedRoot, path.relative(sourceRoot, nativePackage.root)), {
recursive: true,
});
}
await join([
probe("missing", "require", "missing", { FS_SAFE_NATIVE_MODE: "require" }),
...["off", "auto"].map((mode) =>
probe(mode, mode, "fallback", { FS_SAFE_NATIVE_MODE: mode }),
),
]);
}
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
},
);
it.each(
["runtime", "declarations", ...workerBuildTargets.map(([target]) => target)].flatMap((target) =>
[false, true].map((verbose) => ({ target, verbose })),
),
)(
"preserves dependency package boundaries for $target (verbose=$verbose)",
async ({ target, verbose }) => {
vi.stubEnv("OPENCLAW_BUILD_VERBOSE", verbose ? "1" : "0");
const root = fs.realpathSync(createTempDir("openclaw-tsdown-dependencies-"));
const declarations = target === "declarations";
const bundleAll = workerBuildTargets.some(([name]) => name === target);
const selected = bundleAll
? findWorkerBuildConfig(target)
: configs.find(
(entry) =>
entry.name ===
(declarations ? TSDOWN_UNIFIED_DTS_CONFIG_GROUPS[0] : TSDOWN_UNIFIED_CONFIG_GROUP),
);
expect(selected).toBeDefined();
const packages = [
"@anthropic-ai/vertex-sdk",
"@slack/bolt",
"@slack/web-api",
"@discordjs/voice",
"@lancedb/lancedb",
"@larksuiteoapi/node-sdk",
"@matrix-org/matrix-sdk-crypto-nodejs",
"@openclaw/ai",
"@openclaw/crabline",
"@openclaw/fs-safe",
"@vitest/expect",
"jimp",
"matrix-js-sdk",
"prism-media",
"typescript",
"vitest",
"zod",
...Object.keys(
JSON.parse(fs.readFileSync("extensions/memory-lancedb/package.json", "utf8"))
.optionalDependencies,
),
];
// No manifest dependencies: only phantom/transitive copies are resolvable.
// Automatic manifest externalization must not hide a missing build boundary.
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ type: "module" }));
const specifiers: string[] = [];
const expectedImports: string[] = [];
for (const name of packages) {
for (const packageName of [name, `${name}-extra`]) {
const packageRoot = path.join(root, "node_modules", packageName);
fs.mkdirSync(packageRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({
name: packageName,
version: "1.0.0",
type: "module",
exports: {
".": { types: "./index.d.ts", default: "./index.js" },
"./subpath": { types: "./index.d.ts", default: "./index.js" },
},
}),
);
fs.writeFileSync(
path.join(packageRoot, "index.js"),
"export const identity = import.meta.url;\n",
);
fs.writeFileSync(
path.join(packageRoot, "index.d.ts"),
"export interface Identity { value: string }\n",
);
const imports = [packageName, `${packageName}/subpath`];
specifiers.push(...imports);
if (
!bundleAll &&
packageName === name &&
name !== "@lancedb/lancedb" &&
name !== "@openclaw/crabline" &&
(name !== "zod" || declarations)
) {
expectedImports.push(...imports);
}
}
}
const entry = path.join(root, declarations ? "entry.d.ts" : "entry.ts");
fs.writeFileSync(
entry,
specifiers
.map(
(specifier, index) =>
`export { ${declarations ? "type Identity" : "identity"} as value${index} } from ${JSON.stringify(specifier)};`,
)
.join("\n"),
);
if (declarations) {
materializeNativeCompiler(root);
fs.writeFileSync(
path.join(root, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
module: "NodeNext",
target: "ES2023",
types: [],
// The fixture checks bundling of authored declarations, not their type surface.
skipLibCheck: true,
},
files: ["entry.d.ts"],
}),
);
}
const { bundles } = await build({
...selected,
config: false,
cwd: root,
entry: [entry],
outDir: path.join(root, "dist"),
tsconfig: declarations ? path.join(root, "tsconfig.json") : false,
dts: declarations ? { emitDtsOnly: true } : false,
logLevel: "silent",
});
try {
const imports = bundles.flatMap((bundle) =>
bundle.chunks.flatMap((chunk) => (chunk.type === "chunk" ? chunk.imports : [])),
);
expect(imports.toSorted()).toEqual(expectedImports.toSorted());
} finally {
for (const bundle of bundles) {
await bundle[Symbol.asyncDispose]();
}
}
},
);
it.each(["tsdown.config.ts", "tsdown.ai.config.ts"])(
"keeps %s free of runtime imports from tsdown",
(configPath) => {
const source = fs.readFileSync(configPath, "utf8");
expect(source).not.toMatch(/^import(?!\s+type\b).*from ["']tsdown["'];?$/mu);
},
);
it("gives every standalone declaration caller one canonical package config", () => {
for (const packageName of [
"gateway-client",
"gateway-protocol",
"sdk",
"retry",
"normalization-core",
"net-policy",
"media-understanding-common",
"media-generation-core",
"media-core",
"acp-core",
]) {
const manifest = JSON.parse(
fs.readFileSync(`packages/${packageName}/package.json`, "utf8"),
) as { scripts: { build: string }; exports: Record<string, { import: string }> };
expect(manifest.scripts.build).toContain(`build-workspace-package.mts ${packageName}`);
const selected = configs.filter((config) => config.outDir === `packages/${packageName}/dist`);
expect(selected, packageName).toHaveLength(1);
const sources = Object.values(selected[0]!.entry ?? {});
for (const entry of Object.values(manifest.exports)) {
expect(sources).toContain(
entry.import.replace("./dist/", `packages/${packageName}/src/`).replace(/\.mjs$/u, ".ts"),
);
}
if (packageName === "acp-core") {
expect(sources).toContain("packages/acp-core/src/error-format.ts");
}
}
});
it("isolates runtime output from bounded declaration-only graphs", () => {
const packageConfigs = configs.filter((entry) => entry.name === TSDOWN_PACKAGE_CONFIG_GROUP);
const unifiedRuntimeConfig = configs.find(
(entry) => entry.name === TSDOWN_UNIFIED_CONFIG_GROUP,
);
const standaloneRuntimeConfig = configs.find(
(entry) =>
entry.name === TSDOWN_UNIFIED_CONFIG_GROUP &&
entry.dts === false &&
hasWorkerEntry(
entry,
"infra/sqlite-readonly-location.worker",
path.resolve("src/infra/sqlite-readonly-location.worker.ts"),
),
);
const unifiedDeclarationConfigs = TSDOWN_UNIFIED_DTS_CONFIG_GROUPS.map((name) =>
configs.find((entry) => entry.name === name),
);
expect(packageConfigs).not.toHaveLength(0);
expect(packageConfigs.map((entry) => entry.dts)).toEqual(packageConfigs.map(() => true));
expect(unifiedRuntimeConfig?.dts).toBe(false);
expect(standaloneRuntimeConfig?.dts).toBe(false);
expect(unifiedDeclarationConfigs.every(Boolean)).toBe(true);
const runtimeEntries = configs
.filter(
(entry) =>
entry.name === TSDOWN_UNIFIED_CONFIG_GROUP &&
entry !== standaloneRuntimeConfig &&
!isWorkerBuildConfig(entry) &&
!entry.outDir,
)
.flatMap((entry) => Object.entries(entry.entry ?? {}));
const runtimeEntryNames = runtimeEntries.map(([name]) => name);
const runtimeOnlyEntryNames = ["native-hook-relay/entry", "node-host-launcher-bootstrap"];
expect(runtimeEntryNames).toEqual(expect.arrayContaining(runtimeOnlyEntryNames));
const declarationEntries = runtimeEntries.filter(
([name]) => !runtimeOnlyEntryNames.includes(name),
);
const declarationEntryNames = declarationEntries.map(([name]) => name);
const standaloneEntries = Object.entries(standaloneRuntimeConfig?.entry ?? {});
const standaloneNames = new Set(standaloneEntries.map(([name]) => name));
const declarationInputs = Object.fromEntries([...declarationEntries, ...standaloneEntries]);
for (const declarationConfig of unifiedDeclarationConfigs) {
expect(declarationConfig?.dts).toMatchObject({ emitDtsOnly: true });
// Runtime and inventory graphs retain every alias in the declaration input map.
expect(declarationConfig?.entry).toEqual(declarationInputs);
expect(
Object.keys(declarationConfig?.entry ?? {})
.filter((name) => !standaloneNames.has(name))
.toSorted(),
).toEqual(declarationEntryNames.toSorted());
}
});
it("keeps excluded plugins out of the unified graph without dropping host helpers", () => {
const runtime = configs.find((entry) => entry.name === TSDOWN_UNIFIED_CONFIG_GROUP);
const entries = runtime?.entry as Record<string, string>;
const excluded = collectRootPackageExcludedExtensionDirs();
expect(
Object.keys(entries).filter(
(name) => name.startsWith("extensions/") && excluded.has(name.split("/")[1]!),
),
).toEqual([]);
expect(entries["plugin-sdk/codex-mcp-projection"]).toBe(
"src/plugin-sdk/codex-mcp-projection.ts",
);
expect(entries["plugin-sdk/codex-session-transcript-runtime"]).toBe(
"src/plugin-sdk/codex-session-transcript-runtime.ts",
);
expect(entries["plugins/public-surface-runtime"]).toBe("src/plugins/public-surface-runtime.ts");
expect(Object.values(entries)).toEqual(
expect.arrayContaining([
"extensions/vault/vault-secret-id.js",
"extensions/vault/vault-secret-ref-resolver.js",
]),
);
});
it("emits bounded public declarations without private runtime roots", () => {
const declarationSources = TSDOWN_UNIFIED_DTS_CONFIG_GROUPS.flatMap((name) => {
const declarationConfig = configs.find((entry) => entry.name === name);
const dts = declarationConfig?.dts;
if (!dts || typeof dts !== "object" || !Array.isArray(dts.entry)) {
return [];
}
expect(dts.entry.length).toBeLessThanOrEqual(200);
return dts.entry;
});
expect(declarationSources).toEqual(
expect.arrayContaining([
"src/index.ts",
...publicPluginSdkEntrypoints.map((entry) => `src/plugin-sdk/${entry}.ts`),
"extensions/anthropic/api.ts",
"extensions/anthropic/contract-api.ts",
"extensions/memory-core/api.ts",
"extensions/memory-core/runtime-api.ts",
]),
);
expect(
declarationSources.filter(
(source) => source.startsWith("src/") && !source.startsWith("src/plugin-sdk/"),
),
).toEqual(["src/index.ts"]);
expect(
declarationSources.filter((source) => source.startsWith("extensions/anthropic/")).toSorted(),
).toEqual(["extensions/anthropic/api.ts", "extensions/anthropic/contract-api.ts"]);
expect(declarationSources.every((source) => /\.[cm]?tsx?$/u.test(source))).toBe(true);
expect(new Set(declarationSources).size).toBe(declarationSources.length);
});
it("keeps public SDK types canonical without emitting private runtime declarations", () => {
const [publicDeclarationSources = [], privateDeclarationSources = []] =
TSDOWN_UNIFIED_DTS_CONFIG_GROUPS.filter((name) =>
name.startsWith("openclaw-dts-plugin-sdk-"),
).map((name) => {
const dts = configs.find((entry) => entry.name === name)?.dts;
return dts && typeof dts === "object" && Array.isArray(dts.entry) ? dts.entry : [];
});
const publicSources = publicPluginSdkEntrypoints.map((entry) => `src/plugin-sdk/${entry}.ts`);
expect(publicDeclarationSources.toSorted()).toEqual(publicSources.toSorted());
expect(privateDeclarationSources).toEqual([]);
const runtime = configs.find((entry) => entry.name === TSDOWN_UNIFIED_CONFIG_GROUP);
expect(runtime?.entry).toHaveProperty(
"plugin-sdk/tts-runtime",
"src/plugin-sdk/tts-runtime.ts",
);
});
it("builds self-contained worker deploy executables with every dependency bundled", () => {
const workerConfigs = Object.fromEntries(
workerBuildTargets.map(([target, entry, source]) => {
const config = findWorkerBuildConfig(target);
expect(config?.entry).toEqual({ [entry]: source });
return [target, config] as const;
}),
);
const {
worker: workerConfig,
receiver: receiverConfig,
"github-launcher": launcherConfig,
"service-relay": relayConfig,
"service-group-anchor": anchorConfig,
} = workerConfigs;
const packageVersion = (
JSON.parse(fs.readFileSync("package.json", "utf8")) as {
version: string;
}
).version;
expect(workerConfig?.define).toEqual({
WORKER_DEPLOY_BUILD: "true",
SEALED_RUNTIME_BUILD: "true",
WORKER_DEPLOY_VERSION: JSON.stringify(packageVersion),
});
expect(workerConfig?.alias).toMatchObject({
bufferutil: WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
"chromium-bidi/lib/cjs/bidiMapper/BidiMapper": WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
"chromium-bidi/lib/cjs/cdp/CdpConnection": WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
"electron/index.js": WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
fsevents: WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
kerberos: WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
"utf-8-validate": WORKER_DEPLOY_OPTIONAL_NATIVE_MODULE_ID,
});
expect(workerConfig?.outDir).toBe("dist");
expect(workerConfig?.shims).toBe(true);
expect(workerConfig?.plugins).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "openclaw:worker-deploy" })]),
);
expect(workerConfig?.outputOptions).toMatchObject({
codeSplitting: false,
assetFileNames: "worker/[name][extname]",
});
for (const config of [receiverConfig, launcherConfig]) {
expect(config?.define).toBeUndefined();
}
for (const config of [relayConfig, anchorConfig]) {
expect(config?.define).toEqual({
WORKER_DEPLOY_BUILD: "true",
SEALED_RUNTIME_BUILD: "true",
});
}
for (const config of [receiverConfig, launcherConfig, relayConfig, anchorConfig]) {
expect(config?.alias).toBeUndefined();
expect(config?.plugins).toBeUndefined();
expect(config?.outputOptions).toEqual({ codeSplitting: false });
}
const context = {
format: "es",
options: {},
pkgType: "module",
} as Parameters<OutExtensions>[0];
for (const config of Object.values(workerConfigs)) {
expect(config?.dts).toBe(false);
expect(config?.outDir).toBe("dist");
expect(config?.shims).toBe(true);
expect(config?.deps?.onlyBundle).toBe(false);
expect(config?.deps?.alwaysBundle).toBeTypeOf("function");
const alwaysBundle = config?.deps?.alwaysBundle;
if (typeof alwaysBundle !== "function") {
throw new Error("worker deploy config must define dependency bundling");
}
expect(alwaysBundle("json5", undefined)).toBe(true);
expect(config?.platform).toBe("node");
expect(config?.outExtensions?.(context)).toEqual({ js: ".mjs", dts: ".d.ts" });
}
});
it("keeps node package artifacts on the declared js and dts extensions", () => {
const nodePackageConfigs = configs.filter(
(entry) => entry.fixedExtension === false && !isWorkerBuildConfig(entry),
);
expect(nodePackageConfigs).not.toHaveLength(0);
const context = {
format: "es",
options: {},
pkgType: "module",
} as Parameters<OutExtensions>[0];
for (const entry of nodePackageConfigs) {
expect(entry.outExtensions?.(context)).toEqual({ js: ".js", dts: ".d.ts" });
}
});
});