mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* feat(node): update headless runtimes automatically when idle Stage private packaged node runtimes and activate only after all owned work and cleanup settle. Preserve pairing and launch options, keep automatic activations at least 12 hours apart, and leave shared-state migration and Gateway installation ownership with the normal updater. Document defaults, opt-outs, runtime version discovery, and recovery. Support recoverable Windows selector replacement across service restarts. Refs #151462 * fix(node): complete auto-update integration and settings defaults Capture the launcher in compiled test runtimes and trusted wrapper inventories, register the package update proof, and use the canonical Kysely read path. Preserve the inherited enabled state in the config UI and align caller tests with the node shutdown lifecycle. * fix(node): preserve retained plugin work during automatic updates Require an explicit idle result from plugin commands, preserve older-plugin work, and connect bundled lifetime owners to automatic node updates. Join canceled inference and failed terminal cleanup before command settlement; keep historical state repair with Doctor. Document compatibility and extend installed-package proof for retained legacy plugin work. * test(browser): align idle-work fixtures with runtime exports * test(browser): extract proxy request fixtures * test(node): retain idle assertions across native cleanup
1063 lines
44 KiB
TypeScript
1063 lines
44 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 { 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 { 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 isWorkerDeployConfig = (config: TsdownConfig) =>
|
|
hasWorkerEntry(config, "worker/worker", "src/worker/worker-deploy-entry.ts");
|
|
const isWorkerRsyncReceiverConfig = (config: TsdownConfig) =>
|
|
hasWorkerEntry(
|
|
config,
|
|
"worker/workspace-rsync-receiver",
|
|
"src/worker/workspace-rsync-receiver.ts",
|
|
);
|
|
const isWorkerGitHubExecLauncherConfig = (config: TsdownConfig) =>
|
|
hasWorkerEntry(config, "worker/github-exec-launcher", "src/agents/github-exec-launcher.ts");
|
|
const isWorkerBuildConfig = (config: TsdownConfig) =>
|
|
isWorkerDeployConfig(config) ||
|
|
hasWorkerEntry(
|
|
config,
|
|
"worker/image-processor.worker",
|
|
"src/worker/worker-deploy-image-processor.ts",
|
|
) ||
|
|
isWorkerRsyncReceiverConfig(config) ||
|
|
isWorkerGitHubExecLauncherConfig(config);
|
|
|
|
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);
|
|
}
|
|
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("emits every private Telegram QA harness entry only in private QA builds", async () => {
|
|
const expectedEntries = {
|
|
"plugin-sdk/qa-channel-protocol": "src/plugin-sdk/qa-channel-protocol.ts",
|
|
"plugin-sdk/qa-lab": "src/plugin-sdk/qa-lab.ts",
|
|
"plugin-sdk/qa-runtime": "src/plugin-sdk/qa-runtime.ts",
|
|
};
|
|
const defaultUnified = configs.find((config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP);
|
|
expect(defaultUnified?.entry).not.toMatchObject(expectedEntries);
|
|
|
|
vi.stubEnv("OPENCLAW_BUILD_PRIVATE_QA", "1");
|
|
const { default: privateQaBuildConfigs } = await importFreshModule<
|
|
typeof import("../../tsdown.config.ts")
|
|
>(import.meta.url, "../../tsdown.config.ts?private-qa-entries");
|
|
const privateQaUnified = privateQaBuildConfigs.find(
|
|
(config) => config.name === TSDOWN_UNIFIED_CONFIG_GROUP,
|
|
);
|
|
expect(privateQaUnified?.entry).toMatchObject(expectedEntries);
|
|
});
|
|
|
|
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 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];
|
|
expect(source).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! },
|
|
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, 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);
|
|
assert.equal(await db.count("alpha"), 1);
|
|
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`),
|
|
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(["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 = configs.find(
|
|
worker ? isWorkerDeployConfig : (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", "worker", "receiver", "github-launcher"].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 = ["worker", "receiver", "github-launcher"].includes(target);
|
|
const selected = configs.find(
|
|
target === "worker"
|
|
? isWorkerDeployConfig
|
|
: target === "receiver"
|
|
? isWorkerRsyncReceiverConfig
|
|
: target === "github-launcher"
|
|
? isWorkerGitHubExecLauncherConfig
|
|
: (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"),
|
|
);
|
|
const { bundles } = await build({
|
|
...selected,
|
|
config: false,
|
|
cwd: root,
|
|
entry: [entry],
|
|
outDir: path.join(root, "dist"),
|
|
tsconfig: 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("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 workerConfig = configs.find(isWorkerDeployConfig);
|
|
const receiverConfig = configs.find(isWorkerRsyncReceiverConfig);
|
|
const launcherConfig = configs.find(isWorkerGitHubExecLauncherConfig);
|
|
expect(workerConfig?.entry).toEqual({
|
|
"worker/worker": "src/worker/worker-deploy-entry.ts",
|
|
});
|
|
expect(receiverConfig?.entry).toEqual({
|
|
"worker/workspace-rsync-receiver": "src/worker/workspace-rsync-receiver.ts",
|
|
});
|
|
expect(launcherConfig?.entry).toEqual({
|
|
"worker/github-exec-launcher": "src/agents/github-exec-launcher.ts",
|
|
});
|
|
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();
|
|
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 [workerConfig, receiverConfig, launcherConfig]) {
|
|
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(alwaysBundle("node:fs", undefined)).toBe(false);
|
|
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" });
|
|
}
|
|
});
|
|
});
|