refactor: simplify plugin dependency handling

Simplify plugin installation and runtime loading around package-manager-owned dependencies, with Jiti reserved for local/TS fallback paths.

Also scans npm plugin install roots so hoisted transitive dependencies are covered by dependency denylist and node_modules symlink checks.
This commit is contained in:
Peter Steinberger
2026-05-01 21:32:22 +01:00
committed by GitHub
parent 2e8e9cd6ca
commit ed8f50f240
294 changed files with 2562 additions and 25454 deletions

View File

@@ -87,6 +87,14 @@ describe("bundled plugin build entries", () => {
);
});
it("keeps explicitly downloadable plugins out of bundled package artifacts", () => {
const entries = listBundledPluginBuildEntries();
const artifacts = listBundledPluginPackArtifacts();
expect(Object.keys(entries).some((entry) => entry.startsWith("extensions/qqbot/"))).toBe(false);
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/qqbot/"))).toBe(false);
});
it("keeps bundled channel secret contracts on packed top-level sidecars", () => {
const artifacts = listBundledPluginPackArtifacts();
const offenders: string[] = [];

View File

@@ -1,81 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { collectBuiltBundledPluginStagedRuntimeDependencyErrors } from "../../scripts/lib/bundled-plugin-root-runtime-mirrors.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
function writeJson(root: string, relativePath: string, value: unknown) {
const fullPath = path.join(root, relativePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
describe("collectBuiltBundledPluginStagedRuntimeDependencyErrors", () => {
it("flags built staged plugins whose dist node_modules are missing runtime deps", () => {
const repoRoot = createTempDir("openclaw-runtime-contracts-");
writeJson(repoRoot, "dist/extensions/diffs/package.json", {
name: "@openclaw/diffs",
dependencies: {
"@pierre/diffs": "^0.1.0",
},
openclaw: {
bundle: {
stageRuntimeDependencies: true,
},
},
});
expect(
collectBuiltBundledPluginStagedRuntimeDependencyErrors({
bundledPluginsDir: path.join(repoRoot, "dist/extensions"),
}),
).toEqual([
"built bundled plugin 'diffs' is missing staged runtime dependency '@pierre/diffs: ^0.1.0' under dist/extensions/diffs/node_modules.",
]);
});
it("accepts built staged plugins when their staged runtime deps are present", () => {
const repoRoot = createTempDir("openclaw-runtime-contracts-");
writeJson(repoRoot, "dist/extensions/diffs/package.json", {
name: "@openclaw/diffs",
dependencies: {
"@pierre/diffs": "^0.1.0",
},
openclaw: {
bundle: {
stageRuntimeDependencies: true,
},
},
});
writeJson(repoRoot, "dist/extensions/diffs/node_modules/@pierre/diffs/package.json", {
name: "@pierre/diffs",
version: "0.1.0",
});
expect(
collectBuiltBundledPluginStagedRuntimeDependencyErrors({
bundledPluginsDir: path.join(repoRoot, "dist/extensions"),
}),
).toEqual([]);
});
it("keeps the WhatsApp bundled plugin opted into staged runtime dependencies", () => {
const packageJson = JSON.parse(
fs.readFileSync(path.join(process.cwd(), "extensions/whatsapp/package.json"), "utf8"),
) as {
dependencies?: Record<string, string>;
openclaw?: {
bundle?: {
stageRuntimeDependencies?: boolean;
};
};
};
expect(packageJson.dependencies?.["@whiskeysockets/baileys"]).toBe("7.0.0-rc.9");
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
});
});

View File

@@ -14,7 +14,7 @@ import {
} from "../../scripts/lib/local-build-metadata-paths.mjs";
describe("check-gateway-watch-regression", () => {
it("ignores top-level dist-runtime extension dependency repairs", () => {
it("ignores top-level dist-runtime extension dependency debris", () => {
expect(isIgnoredDistRuntimeWatchPath("dist-runtime/extensions/node_modules")).toBe(true);
expect(
isIgnoredDistRuntimeWatchPath(

View File

@@ -144,8 +144,6 @@ describe("docker build helper", () => {
expect(scenarios).toContain("`bundled-plugin-install-uninstall-${index}`");
expect(scenarios).toContain("pnpm test:docker:bundled-plugin-install-uninstall");
expect(scenarios).toContain("OPENCLAW_PLUGINS_E2E_CLAWHUB=0");
expect(scenarios).toContain('"bundled-channel-deps-compat"');
expect(scenarios).toContain("test:docker:bundled-channel-deps:fast");
});
it("allows plugin update smoke to tolerate config metadata migrations", () => {

View File

@@ -46,8 +46,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
expect(plan.lanes.map((lane) => lane.name)).toContain("install-e2e-anthropic");
expect(plan.lanes.map((lane) => lane.name)).toContain("mcp-channels");
expect(plan.lanes.map((lane) => lane.name)).toContain("commitments-safety");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-channel-feishu");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-channel-update-acpx");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-0");
expect(plan.lanes.map((lane) => lane.name)).toContain("bundled-plugin-install-uninstall-23");
expect(plan.lanes.filter((lane) => lane.name === "install-e2e-openai")).toHaveLength(1);
@@ -141,31 +139,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-h",
});
const bundledChannelsCore = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-core",
});
const bundledChannelsUpdateA = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-a",
});
const bundledChannelsUpdateDiscord = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-discord",
});
const bundledChannelsUpdateB = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-b",
});
const bundledChannelsContracts = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-contracts",
});
expect(packageInstallOpenAi.lanes.map((lane) => lane.name)).toEqual(["install-e2e-openai"]);
expect(packageInstallAnthropic.lanes.map((lane) => lane.name)).toEqual([
@@ -260,42 +233,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
"bundled-plugin-install-uninstall-22",
"bundled-plugin-install-uninstall-23",
]);
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).toEqual([
"plugin-update",
"bundled-channel-telegram",
"bundled-channel-discord",
"bundled-channel-slack",
"bundled-channel-feishu",
"bundled-channel-memory-lancedb",
]);
expect(bundledChannelsCore.lanes[0]).toMatchObject({
name: "plugin-update",
stateScenario: "empty",
});
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-telegram",
"bundled-channel-update-memory-lancedb",
]);
expect(bundledChannelsUpdateDiscord.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-discord",
]);
expect(bundledChannelsUpdateDiscord.lanes[0]).toMatchObject({
noOutputTimeoutMs: 4 * 60 * 1000,
timeoutMs: 6 * 60 * 1000,
});
expect(bundledChannelsUpdateB.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-slack",
"bundled-channel-update-feishu",
"bundled-channel-update-acpx",
]);
expect(bundledChannelsContracts.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-root-owned",
"bundled-channel-setup-entry",
"bundled-channel-load-failure",
"bundled-channel-disabled-config",
]);
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).not.toContain("plugins");
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).not.toContain("openwebui");
});
it("keeps legacy release chunk names as aggregate aliases", () => {
@@ -309,11 +246,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime",
});
const bundledChannelsUpdateALegacy = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-a-legacy",
});
const legacy = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
@@ -335,19 +267,8 @@ describe("scripts/lib/docker-e2e-plan", () => {
"openwebui",
]),
);
expect(bundledChannelsUpdateALegacy.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-telegram",
"bundled-channel-update-discord",
"bundled-channel-update-memory-lancedb",
]);
expect(legacy.lanes.map((lane) => lane.name)).toEqual(
expect.arrayContaining([
"plugins",
"bundled-plugin-install-uninstall-0",
"plugin-update",
"bundled-channel-update-acpx",
"openwebui",
]),
expect.arrayContaining(["plugins", "bundled-plugin-install-uninstall-0", "openwebui"]),
);
});
@@ -487,8 +408,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
"plugin-update",
"plugins",
"kitchen-sink-plugin",
"bundled-channel-deps-compat",
"bundled-channel-setup-entry",
"bundled-plugin-install-uninstall-0",
"commitments-safety",
"update-channel-switch",
@@ -557,14 +476,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
name: "kitchen-sink-plugin",
stateScenario: "empty",
}),
expect.objectContaining({
name: "bundled-channel-deps-compat",
stateScenario: "empty",
}),
expect.objectContaining({
name: "bundled-channel-setup-entry",
stateScenario: "empty",
}),
expect.objectContaining({
name: "bundled-plugin-install-uninstall-0",
stateScenario: "empty",
@@ -584,19 +495,6 @@ describe("scripts/lib/docker-e2e-plan", () => {
]);
});
it("maps the legacy bundled channel deps lane to the split compat lane", () => {
const selectedLaneNames = parseLaneSelection("bundled-channel-deps");
const plan = planFor({ selectedLaneNames });
expect(selectedLaneNames).toEqual(["bundled-channel-deps-compat"]);
expect(plan.lanes).toEqual([
expect.objectContaining({
imageKind: "bare",
name: "bundled-channel-deps-compat",
}),
]);
});
it("maps installer E2E to provider-specific package install lanes", () => {
const selectedLaneNames = parseLaneSelection("install-e2e");
const plan = planFor({ selectedLaneNames });

View File

@@ -98,17 +98,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
}
});
it("retries transient bundled runtime deps staging failures during agent turns", () => {
expect(
shouldRetryCrossOsAgentTurnError(
new Error("document-extract: failed to install bundled runtime deps: npm install failed"),
),
).toBe(true);
expect(
shouldRetryCrossOsAgentTurnError(
new Error("document-extract failed to stage bundled runtime deps after 463ms"),
),
).toBe(true);
it("retries transient agent-turn failures", () => {
expect(
shouldRetryCrossOsAgentTurnError(
new Error("Agent output did not contain the expected OK marker."),
@@ -671,7 +661,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
}
});
it("rejects bundled runtime-deps staging debris before candidate inventory generation", async () => {
it("rejects legacy plugin dependency staging debris before candidate inventory generation", async () => {
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-stage-debris-"));
try {
mkdirSync(
@@ -689,7 +679,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
sourceDir: packageRoot,
logPath: join(packageRoot, "npm-pack-dry-run.log"),
}),
).rejects.toThrow("unexpected bundled-runtime-deps install staging debris");
).rejects.toThrow("unexpected legacy plugin dependency staging debris");
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}

View File

@@ -52,7 +52,6 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("npm-onboard-channel-agent doctor-switch");
expect(workflow).toContain("update-channel-switch upgrade-survivor");
expect(workflow).toContain("published-upgrade-survivor");
expect(workflow).toContain("bundled-channel-deps-compat");
expect(workflow).toContain("plugins-offline plugin-update");
expect(workflow).toContain("include_release_path_suites=true");
expect(workflow).not.toContain("telegram_mode requires source=npm");
@@ -376,7 +375,7 @@ describe("package artifact reuse", () => {
"package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}",
);
expect(workflow).toContain("suite_profile: custom");
expect(workflow).toContain("docker_lanes: bundled-channel-deps-compat plugins-offline");
expect(workflow).toContain("docker_lanes: plugins-offline plugin-update");
expect(workflow).toContain("telegram_mode: mock-openai");
expect(workflow).toContain(
"telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-mention-gating",

View File

@@ -428,7 +428,7 @@ console.log(JSON.stringify(result));
expect(macos).not.toContain("Authorization: Bot");
expect(discord).toContain("Authorization: Bot");
expect(discord).toContain('"--silent"');
expect(discord).toContain("plugins deps --repair");
expect(discord).toContain("doctor --fix --yes --non-interactive");
expect(discord).toContain("channels status --probe --json");
expect(discord).toContain("Stop ${this.input.vmName} after successful Discord smoke");
});

View File

@@ -38,7 +38,6 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
"npm-onboard-channel-agent",
"doctor-switch",
"update-channel-switch",
"bundled-channel-deps-compat",
"plugins-offline",
"plugins",
"kitchen-sink-plugin",
@@ -128,12 +127,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
expect(assertionsScript).toContain("assertClawHubExternalInstallContract");
expect(assertionsScript).toContain("expectedErrorMessages");
expect(assertionsScript).toContain(
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "adversarial"]);',
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "conformance", "adversarial"]);',
);
expect(assertionsScript).toContain("!INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES.has(surfaceMode)");
expect(assertionsScript).not.toContain(
'const INVALID_PROBE_DIAGNOSTIC_SURFACE_MODES = new Set(["full", "conformance"',
);
expect(readFileSync("scripts/e2e/lib/clawhub-fixture-server.cjs", "utf8")).toContain(
'from "openclaw/plugin-sdk/plugin-entry"',
);

View File

@@ -2,16 +2,10 @@ import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
createBundledRuntimeDependencyInstallArgs,
createBundledRuntimeDependencyInstallEnv,
createNestedNpmInstallEnv,
} from "../../scripts/lib/bundled-runtime-deps-install.mjs";
import {
isDirectPostinstallInvocation,
pruneOpenClawCompileCache,
pruneInstalledPackageDist,
discoverBundledPluginRuntimeDeps,
pruneBundledPluginSourceNodeModules,
runBundledPluginPostinstall,
runPluginRegistryPostinstallMigration,
@@ -67,77 +61,6 @@ describe("bundled plugin postinstall", () => {
).toBe(true);
});
async function writeDiscordDaveyOptionalDependencyFixture(
extensionsDir: string,
packageRoot: string,
) {
await writePluginPackage(extensionsDir, "discord", {
dependencies: {
"@snazzah/davey": "0.1.11",
},
});
await fs.mkdir(path.join(packageRoot, "node_modules", "@snazzah", "davey"), {
recursive: true,
});
await fs.writeFile(
path.join(packageRoot, "node_modules", "@snazzah", "davey", "package.json"),
JSON.stringify({
optionalDependencies: {
"@snazzah/davey-win32-arm64-msvc": "0.1.11",
},
}),
);
}
it("clears global npm config before nested installs", () => {
expect(
createNestedNpmInstallEnv({
NPM_CONFIG_WORKSPACES: "true",
npm_config_global: "true",
npm_config_include_workspace_root: "true",
npm_config_ignore_scripts: "false",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
npm_config_workspace: "extensions/telegram",
npm_config_workspaces: "true",
HOME: "/tmp/home",
}),
).toEqual({
HOME: "/tmp/home",
});
});
it("uses package-manager-neutral runtime install args with npm config env", () => {
expect(createBundledRuntimeDependencyInstallArgs(["acpx@0.4.1"])).toEqual([
"install",
"--ignore-scripts",
"--workspaces=false",
"acpx@0.4.1",
]);
expect(
createBundledRuntimeDependencyInstallEnv({
HOME: "/tmp/home",
NPM_CONFIG_IGNORE_SCRIPTS: "false",
npm_config_dry_run: "true",
npm_config_ignore_scripts: "false",
npm_config_prefix: "/opt/homebrew",
npm_config_workspaces: "true",
}),
).toEqual({
HOME: "/tmp/home",
npm_config_dry_run: "false",
npm_config_fetch_retries: "5",
npm_config_fetch_retry_maxtimeout: "120000",
npm_config_fetch_retry_mintimeout: "10000",
npm_config_fetch_timeout: "300000",
npm_config_ignore_scripts: "true",
npm_config_legacy_peer_deps: "true",
npm_config_package_lock: "false",
npm_config_save: "false",
npm_config_workspaces: "false",
});
});
it("does not install bundled plugin deps outside of source checkouts by default", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
@@ -717,81 +640,6 @@ describe("bundled plugin postinstall", () => {
expect(spawnSync).not.toHaveBeenCalled();
});
it("does not reinstall when only another platform optional native child is missing", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writeDiscordDaveyOptionalDependencyFixture(extensionsDir, packageRoot);
const spawnSync = vi.fn();
runBundledPluginPostinstall({
env: { HOME: "/tmp/home" },
extensionsDir,
packageRoot,
arch: "arm64",
platform: "darwin",
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expect(spawnSync).not.toHaveBeenCalled();
});
it("discovers bundled plugin runtime deps from extension manifests", async () => {
const extensionsDir = await createExtensionsDir();
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"@slack/web-api": "7.11.0",
},
});
await writePluginPackage(extensionsDir, "amazon-bedrock", {
dependencies: {
"@aws-sdk/client-bedrock": "3.1020.0",
},
});
expect(discoverBundledPluginRuntimeDeps({ extensionsDir })).toEqual(
expect.arrayContaining([
{
name: "@slack/web-api",
pluginIds: ["slack"],
sentinelPath: path.join("node_modules", "@slack", "web-api", "package.json"),
version: "7.11.0",
},
{
name: "@aws-sdk/client-bedrock",
pluginIds: ["amazon-bedrock"],
sentinelPath: path.join("node_modules", "@aws-sdk", "client-bedrock", "package.json"),
version: "3.1020.0",
},
]),
);
});
it("merges duplicate bundled runtime deps across plugins", async () => {
const extensionsDir = await createExtensionsDir();
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"https-proxy-agent": "^8.0.0",
},
});
await writePluginPackage(extensionsDir, "feishu", {
dependencies: {
"https-proxy-agent": "^8.0.0",
},
});
expect(discoverBundledPluginRuntimeDeps({ extensionsDir })).toEqual(
expect.arrayContaining([
{
name: "https-proxy-agent",
pluginIds: ["feishu", "slack"],
sentinelPath: path.join("node_modules", "https-proxy-agent", "package.json"),
version: "^8.0.0",
},
]),
);
});
it("prunes only bundled plugin package node_modules in source checkouts", async () => {
const packageRoot = await createTempDirAsync("openclaw-source-prune-");
const extensionsDir = path.join(packageRoot, "extensions");

View File

@@ -63,24 +63,10 @@ describe("collectModuleSpecifiers", () => {
});
describe("classifyRootDependencyOwnership", () => {
it("treats root-dist bundled runtime imports as localizable extension deps", () => {
expect(
classifyRootDependencyOwnership({
sections: ["extensions"],
rootMirrorImporters: ["discovery-DZDwKJdJ.js"],
}),
).toEqual({
category: "extension_only_localizable",
recommendation:
"remove from root package.json and rely on owning extension manifests plus doctor --fix",
});
});
it("treats scripts and tests as dev-only candidates", () => {
expect(
classifyRootDependencyOwnership({
sections: ["scripts", "test"],
rootMirrorImporters: [],
}),
).toEqual({
category: "script_or_test_only",
@@ -88,11 +74,11 @@ describe("classifyRootDependencyOwnership", () => {
});
});
it("treats extension-only deps as localizable when no root mirror exists", () => {
it("treats extension-only deps as localizable", () => {
expect(
classifyRootDependencyOwnership({
depName: "vendor-sdk",
sections: ["extensions", "test"],
rootMirrorImporters: [],
}),
).toEqual({
category: "extension_only_localizable",
@@ -101,11 +87,23 @@ describe("classifyRootDependencyOwnership", () => {
});
});
it("allows explicit root-owned internal extension runtime dependencies", () => {
expect(
classifyRootDependencyOwnership({
depName: "playwright-core",
sections: ["extensions", "test"],
}),
).toEqual({
category: "root_owned_extension_runtime",
recommendation:
"keep at root; the internal browser runtime is shipped with core even though downloadable browser-adjacent plugins also declare it",
});
});
it("treats src-owned deps as core runtime", () => {
expect(
classifyRootDependencyOwnership({
sections: ["src"],
rootMirrorImporters: [],
}),
).toEqual({
category: "core_runtime",
@@ -117,7 +115,6 @@ describe("classifyRootDependencyOwnership", () => {
expect(
classifyRootDependencyOwnership({
sections: [],
rootMirrorImporters: [],
}),
).toEqual({
category: "unreferenced",
@@ -224,4 +221,34 @@ describe("collectRootDependencyOwnershipCheckErrors", () => {
"root dependency '@tencent-connect/qqbot-connector' is extension-owned (remove from root package.json and rely on owning extension manifests plus doctor --fix); extension declarations: qqbot:dependencies; sample imports: extensions/qqbot/src/bridge/setup/finalize.ts",
]);
});
it("does not fail explicitly root-owned internal extension runtime dependencies", () => {
const repoRoot = makeTempRepo();
writeRepoFile(
repoRoot,
"package.json",
JSON.stringify({ dependencies: { "playwright-core": "1.59.1" } }),
);
writeRepoFile(
repoRoot,
"extensions/browser/package.json",
JSON.stringify({ dependencies: { "playwright-core": "1.59.1" } }),
);
writeRepoFile(
repoRoot,
"extensions/browser/src/browser/playwright-core.runtime.ts",
'const runtime = require("playwright-core");\n',
);
const records = collectRootDependencyOwnershipAudit({ repoRoot, scanRoots: ["extensions"] });
expect(records).toMatchObject([
{
category: "root_owned_extension_runtime",
depName: "playwright-core",
sections: ["extensions"],
},
]);
expect(collectRootDependencyOwnershipCheckErrors(records)).toEqual([]);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -106,68 +106,18 @@ describe("resolveTsdownBuildInvocation", () => {
).rejects.toThrow();
});
it("cleans tsdown output roots before using tsdown --no-clean without deleting staged runtime deps", async () => {
it("cleans tsdown output roots before using tsdown --no-clean", async () => {
const rootDir = createTempDir("openclaw-tsdown-clean-");
const distFile = path.join(rootDir, "dist", "stale.js");
const pluginManifest = path.join(rootDir, "extensions", "telegram", "openclaw.plugin.json");
const pluginSourceManifest = path.join(rootDir, "extensions", "telegram", "package.json");
const pluginGeneratedFile = path.join(rootDir, "dist", "extensions", "telegram", "index.js");
const pluginRuntimeDepFile = path.join(
rootDir,
"dist",
"extensions",
"telegram",
"node_modules",
"grammy",
"package.json",
);
const stalePluginRuntimeDepFile = path.join(
rootDir,
"dist",
"extensions",
"old-plugin",
"node_modules",
"left-pad",
"package.json",
);
const unstagedPluginSourceManifest = path.join(
rootDir,
"extensions",
"unstaged-plugin",
"package.json",
);
const unstagedPluginRuntimeDepFile = path.join(
rootDir,
"dist",
"extensions",
"unstaged-plugin",
"node_modules",
"left-pad",
"package.json",
);
const distRuntimeFile = path.join(rootDir, "dist-runtime", "stale.js");
const unrelatedFile = path.join(rootDir, "tmp", "keep.js");
await fsPromises.mkdir(path.dirname(distFile), { recursive: true });
await fsPromises.mkdir(path.dirname(pluginManifest), { recursive: true });
await fsPromises.mkdir(path.dirname(pluginSourceManifest), { recursive: true });
await fsPromises.mkdir(path.dirname(pluginGeneratedFile), { recursive: true });
await fsPromises.mkdir(path.dirname(pluginRuntimeDepFile), { recursive: true });
await fsPromises.mkdir(path.dirname(stalePluginRuntimeDepFile), { recursive: true });
await fsPromises.mkdir(path.dirname(unstagedPluginSourceManifest), { recursive: true });
await fsPromises.mkdir(path.dirname(unstagedPluginRuntimeDepFile), { recursive: true });
await fsPromises.mkdir(path.dirname(distRuntimeFile), { recursive: true });
await fsPromises.mkdir(path.dirname(unrelatedFile), { recursive: true });
await fsPromises.writeFile(distFile, "stale\n");
await fsPromises.writeFile(pluginManifest, '{"id":"telegram"}\n');
await fsPromises.writeFile(
pluginSourceManifest,
'{"openclaw":{"bundle":{"stageRuntimeDependencies":true}}}\n',
);
await fsPromises.writeFile(pluginGeneratedFile, "generated\n");
await fsPromises.writeFile(pluginRuntimeDepFile, "{}\n");
await fsPromises.writeFile(stalePluginRuntimeDepFile, "{}\n");
await fsPromises.writeFile(unstagedPluginSourceManifest, "{}\n");
await fsPromises.writeFile(unstagedPluginRuntimeDepFile, "{}\n");
await fsPromises.writeFile(distRuntimeFile, "stale\n");
await fsPromises.writeFile(unrelatedFile, "keep\n");
@@ -175,13 +125,6 @@ describe("resolveTsdownBuildInvocation", () => {
await expect(fsPromises.stat(distFile)).rejects.toThrow();
await expect(fsPromises.stat(pluginGeneratedFile)).rejects.toThrow();
await expect(fsPromises.readFile(pluginRuntimeDepFile, "utf8")).resolves.toBe("{}\n");
await expect(
fsPromises.stat(path.join(rootDir, "dist", "extensions", "old-plugin")),
).rejects.toThrow();
await expect(
fsPromises.stat(path.join(rootDir, "dist", "extensions", "unstaged-plugin")),
).rejects.toThrow();
await expect(fsPromises.stat(path.join(rootDir, "dist-runtime"))).rejects.toThrow();
await expect(fsPromises.readFile(unrelatedFile, "utf8")).resolves.toBe("keep\n");
});