release: mirror bundled channel deps at root (#63065)

Merged via squash.

Prepared head SHA: ac26799a54
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob
This commit is contained in:
scoootscooob
2026-04-08 04:00:17 -07:00
committed by GitHub
parent 9bf3482470
commit d52d5ad6ff
11 changed files with 488 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
- Slack/media: preserve bearer auth across same-origin `files.slack.com` redirects while still stripping it on cross-origin Slack CDN hops, so `url_private_download` image attachments load again. (#62960) Thanks @vincentkoc.
- Control UI: guard stale session-history reloads during fast session switches so the selected session and rendered transcript stay in sync. (#62975) Thanks @scoootscooob.
- Agents/failover: classify Z.ai vendor code `1311` as billing and `1113` as auth, including long wrapped `1311` payloads, so these errors stop falling through to generic failover handling. (#49552) Thanks @1bcMax.
- npm packaging: mirror bundled Slack, Telegram, Discord, and Feishu channel runtime deps at the root and harden published-install verification so fresh installs fail fast on manifest drift instead of missing-module crashes. (#63065) Thanks @scoootscooob.
## 2026.4.8

View File

@@ -60,6 +60,14 @@
"bundle": {
"stageRuntimeDependencies": true
},
"releaseChecks": {
"rootDependencyMirrorAllowlist": [
"@buape/carbon",
"@discordjs/opus",
"https-proxy-agent",
"opusscript"
]
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -51,6 +51,11 @@
"bundle": {
"stageRuntimeDependencies": true
},
"releaseChecks": {
"rootDependencyMirrorAllowlist": [
"@larksuiteoapi/node-sdk"
]
},
"release": {
"publishToClawHub": true,
"publishToNpm": true

View File

@@ -34,6 +34,13 @@
},
"bundle": {
"stageRuntimeDependencies": true
},
"releaseChecks": {
"rootDependencyMirrorAllowlist": [
"@slack/bolt",
"@slack/web-api",
"https-proxy-agent"
]
}
}
}

View File

@@ -39,6 +39,13 @@
},
"bundle": {
"stageRuntimeDependencies": true
},
"releaseChecks": {
"rootDependencyMirrorAllowlist": [
"@grammyjs/runner",
"@grammyjs/transformer-throttler",
"grammy"
]
}
}
}

View File

@@ -1311,8 +1311,12 @@
"@aws-sdk/client-bedrock": "3.1024.0",
"@aws-sdk/client-bedrock-runtime": "3.1024.0",
"@aws-sdk/credential-provider-node": "3.972.29",
"@buape/carbon": "0.14.0",
"@clack/prompts": "^1.2.0",
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.6",
"@larksuiteoapi/node-sdk": "^1.60.0",
"@line/bot-sdk": "^11.0.0",
"@lydell/node-pty": "1.2.0-beta.10",
"@mariozechner/pi-agent-core": "0.65.2",
@@ -1323,6 +1327,8 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@mozilla/readability": "^0.6.0",
"@sinclair/typebox": "0.34.49",
"@slack/bolt": "^4.6.0",
"@slack/web-api": "^7.15.0",
"ajv": "^8.18.0",
"chalk": "^5.6.2",
"chokidar": "^5.0.0",
@@ -1333,7 +1339,9 @@
"express": "^5.2.1",
"file-type": "22.0.0",
"gaxios": "7.1.4",
"grammy": "^1.42.0",
"hono": "4.12.10",
"https-proxy-agent": "^9.0.0",
"ipaddr.js": "^2.3.0",
"jiti": "^2.6.1",
"json5": "^2.2.3",
@@ -1343,6 +1351,7 @@
"markdown-it": "^14.1.1",
"matrix-js-sdk": "41.3.0-rc.0",
"node-edge-tts": "^1.2.10",
"opusscript": "^0.1.1",
"osc-progress": "^0.3.0",
"pdfjs-dist": "^5.6.205",
"playwright-core": "1.59.1",
@@ -1392,6 +1401,7 @@
}
},
"optionalDependencies": {
"@discordjs/opus": "^0.10.0",
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
"openshell": "0.1.0"
},

32
pnpm-lock.yaml generated
View File

@@ -45,12 +45,24 @@ importers:
'@aws-sdk/credential-provider-node':
specifier: 3.972.29
version: 3.972.29
'@buape/carbon':
specifier: 0.14.0
version: 0.14.0(@discordjs/opus@0.10.0)(hono@4.12.10)(opusscript@0.1.1)
'@clack/prompts':
specifier: ^1.2.0
version: 1.2.0
'@grammyjs/runner':
specifier: ^2.0.3
version: 2.0.3(grammy@1.42.0)
'@grammyjs/transformer-throttler':
specifier: ^1.2.1
version: 1.2.1(grammy@1.42.0)
'@homebridge/ciao':
specifier: ^1.3.6
version: 1.3.6
'@larksuiteoapi/node-sdk':
specifier: ^1.60.0
version: 1.60.0
'@line/bot-sdk':
specifier: ^11.0.0
version: 11.0.0
@@ -84,6 +96,12 @@ importers:
'@sinclair/typebox':
specifier: 0.34.49
version: 0.34.49
'@slack/bolt':
specifier: ^4.6.0
version: 4.6.0(@types/express@5.0.6)
'@slack/web-api':
specifier: ^7.15.0
version: 7.15.0
ajv:
specifier: ^8.18.0
version: 8.18.0
@@ -114,9 +132,15 @@ importers:
gaxios:
specifier: 7.1.4
version: 7.1.4
grammy:
specifier: ^1.42.0
version: 1.42.0
hono:
specifier: 4.12.10
version: 4.12.10
https-proxy-agent:
specifier: ^9.0.0
version: 9.0.0
ipaddr.js:
specifier: ^2.3.0
version: 2.3.0
@@ -147,6 +171,9 @@ importers:
node-llama-cpp:
specifier: 3.18.1
version: 3.18.1(typescript@6.0.2)
opusscript:
specifier: ^0.1.1
version: 0.1.1
osc-progress:
specifier: ^0.3.0
version: 0.3.0
@@ -257,6 +284,9 @@ importers:
specifier: ^4.1.2
version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/browser-playwright@4.1.2)(jsdom@29.0.1(@noble/hashes@2.0.1))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))
optionalDependencies:
'@discordjs/opus':
specifier: ^0.10.0
version: 0.10.0
'@matrix-org/matrix-sdk-crypto-nodejs':
specifier: ^0.4.0
version: 0.4.0
@@ -9807,7 +9837,7 @@ snapshots:
dependencies:
'@slack/logger': 4.0.1
'@slack/types': 2.20.1
'@types/node': 25.5.0
'@types/node': 25.5.2
'@types/retry': 0.12.0
axios: 1.13.6
eventemitter3: 5.0.4

View File

@@ -1,9 +1,17 @@
#!/usr/bin/env -S node --import tsx
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import {
existsSync,
lstatSync,
mkdtempSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isAbsolute, join, relative } from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
@@ -11,8 +19,27 @@ import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm
type InstalledPackageJson = {
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
type InstalledBundledExtensionPackageJson = {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
releaseChecks?: {
rootDependencyMirrorAllowlist?: unknown;
};
};
};
type InstalledBundledExtensionManifestRecord = {
manifest: InstalledBundledExtensionPackageJson;
path: string;
};
const MAX_BUNDLED_EXTENSION_MANIFEST_BYTES = 1024 * 1024;
export type PublishedInstallScenario = {
name: string;
installSpecs: string[];
@@ -64,6 +91,141 @@ export function collectInstalledPackageErrors(params: {
}
}
errors.push(...collectInstalledMirroredRootDependencyManifestErrors(params.packageRoot));
return errors;
}
export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
return platform === "win32"
? join(prefixDir, "openclaw.cmd")
: join(prefixDir, "bin", "openclaw");
}
function collectRuntimeDependencySpecs(packageJson: InstalledPackageJson): Map<string, string> {
return new Map([
...Object.entries(packageJson.dependencies ?? {}),
...Object.entries(packageJson.optionalDependencies ?? {}),
]);
}
function readBundledExtensionPackageJsons(packageRoot: string): {
manifests: InstalledBundledExtensionManifestRecord[];
errors: string[];
} {
const extensionsDir = join(packageRoot, "dist", "extensions");
if (!existsSync(extensionsDir)) {
return { manifests: [], errors: [] };
}
const manifests: InstalledBundledExtensionManifestRecord[] = [];
const errors: string[] = [];
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const extensionDirPath = join(extensionsDir, entry.name);
const packageJsonPath = join(extensionsDir, entry.name, "package.json");
if (!existsSync(packageJsonPath)) {
errors.push(`installed bundled extension manifest missing: ${packageJsonPath}.`);
continue;
}
try {
const packageJsonStats = lstatSync(packageJsonPath);
if (!packageJsonStats.isFile()) {
throw new Error("manifest must be a regular file");
}
if (packageJsonStats.size > MAX_BUNDLED_EXTENSION_MANIFEST_BYTES) {
throw new Error(`manifest exceeds ${MAX_BUNDLED_EXTENSION_MANIFEST_BYTES} bytes`);
}
const realExtensionDirPath = realpathSync(extensionDirPath);
const realPackageJsonPath = realpathSync(packageJsonPath);
const relativeManifestPath = relative(realExtensionDirPath, realPackageJsonPath);
if (
relativeManifestPath.length === 0 ||
relativeManifestPath.startsWith("..") ||
isAbsolute(relativeManifestPath)
) {
throw new Error("manifest resolves outside the bundled extension directory");
}
manifests.push({
manifest: JSON.parse(
readFileSync(realPackageJsonPath, "utf8"),
) as InstalledBundledExtensionPackageJson,
path: realPackageJsonPath,
});
} catch (error) {
errors.push(
`installed bundled extension manifest invalid: failed to parse ${packageJsonPath}: ${formatErrorMessage(error)}.`,
);
}
}
return { manifests, errors };
}
export function collectInstalledMirroredRootDependencyManifestErrors(
packageRoot: string,
): string[] {
const packageJsonPath = join(packageRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return ["installed package is missing package.json."];
}
const rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackageJson);
const { manifests, errors } = readBundledExtensionPackageJsons(packageRoot);
for (const { manifest: extensionPackageJson } of manifests) {
const allowlist = extensionPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
if (allowlist === undefined) {
continue;
}
if (!Array.isArray(allowlist)) {
errors.push(
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array.",
);
continue;
}
const extensionRuntimeDeps = collectRuntimeDependencySpecs(extensionPackageJson);
for (const entry of allowlist) {
if (typeof entry !== "string" || entry.trim().length === 0) {
errors.push(
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist entries must be non-empty strings.",
);
continue;
}
const extensionSpec = extensionRuntimeDeps.get(entry);
if (!extensionSpec) {
errors.push(
`installed bundled extension manifest invalid: mirrored dependency '${entry}' must be declared in the extension runtime dependencies.`,
);
continue;
}
const rootSpec = rootRuntimeDeps.get(entry);
if (!rootSpec) {
errors.push(
`installed package is missing mirrored root runtime dependency '${entry}' required by a bundled extension.`,
);
continue;
}
if (rootSpec !== extensionSpec) {
errors.push(
`installed package mirrored dependency '${entry}' version mismatch: root '${rootSpec}', extension '${extensionSpec}'.`,
);
}
}
}
return errors;
}
@@ -89,6 +251,15 @@ function installSpec(prefixDir: string, spec: string, cwd: string): void {
npmExec(["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"], cwd);
}
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
return execFileSync(resolveInstalledBinaryPath(prefixDir), ["--version"], {
cwd,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
}
function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
const prefixDir = join(workingDir, "prefix");
@@ -108,6 +279,13 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
installedVersion: pkg.version?.trim() ?? "",
packageRoot,
});
const installedBinaryVersion = readInstalledBinaryVersion(prefixDir, workingDir);
if (installedBinaryVersion !== scenario.expectedVersion) {
errors.push(
`installed openclaw binary version mismatch: expected ${scenario.expectedVersion}, found ${installedBinaryVersion || "<missing>"}.`,
);
}
if (errors.length > 0) {
throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`);

View File

@@ -6,12 +6,17 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
{ pluginId: "bluebubbles", minHostVersionBaseline: "2026.3.22" },
{
pluginId: "discord",
runtimeDeps: ["@buape/carbon", "https-proxy-agent"],
mirroredRootRuntimeDeps: [
"@buape/carbon",
"@discordjs/opus",
"https-proxy-agent",
"opusscript",
],
minHostVersionBaseline: "2026.3.22",
},
{
pluginId: "feishu",
runtimeDeps: ["@larksuiteoapi/node-sdk"],
mirroredRootRuntimeDeps: ["@larksuiteoapi/node-sdk"],
minHostVersionBaseline: "2026.3.22",
},
{ pluginId: "googlechat", minHostVersionBaseline: "2026.3.22" },
@@ -21,21 +26,27 @@ const packageManifestContractTests: PackageManifestContractParams[] = [
{ pluginId: "mattermost", minHostVersionBaseline: "2026.3.22" },
{
pluginId: "memory-lancedb",
runtimeDeps: ["@lancedb/lancedb"],
pluginLocalRuntimeDeps: ["@lancedb/lancedb"],
minHostVersionBaseline: "2026.3.22",
},
{ pluginId: "msteams", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "nextcloud-talk", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "nostr", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "slack", runtimeDeps: ["@slack/bolt"] },
{
pluginId: "slack",
mirroredRootRuntimeDeps: ["@slack/bolt", "@slack/web-api", "https-proxy-agent"],
},
{ pluginId: "synology-chat", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "telegram", runtimeDeps: ["grammy"] },
{
pluginId: "telegram",
mirroredRootRuntimeDeps: ["@grammyjs/runner", "@grammyjs/transformer-throttler", "grammy"],
},
{ pluginId: "tlon", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "twitch", minHostVersionBaseline: "2026.3.22" },
{ pluginId: "voice-call", minHostVersionBaseline: "2026.3.22" },
{
pluginId: "whatsapp",
runtimeDeps: ["@whiskeysockets/baileys", "jimp"],
pluginLocalRuntimeDeps: ["@whiskeysockets/baileys", "jimp"],
minHostVersionBaseline: "2026.3.22",
},
{ pluginId: "zalo", minHostVersionBaseline: "2026.3.22" },

View File

@@ -7,16 +7,21 @@ import { bundledPluginFile } from "../bundled-plugin-paths.js";
type PackageManifest = {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
install?: {
minHostVersion?: string;
};
releaseChecks?: {
rootDependencyMirrorAllowlist?: unknown;
};
};
};
type PackageManifestContractParams = {
pluginId: string;
runtimeDeps?: string[];
pluginLocalRuntimeDeps?: string[];
mirroredRootRuntimeDeps?: string[];
minHostVersionBaseline?: string;
};
@@ -29,13 +34,17 @@ export function describePackageManifestContract(params: PackageManifestContractP
const packagePath = bundledPluginFile(params.pluginId, "package.json");
describe(`${params.pluginId} package manifest contract`, () => {
if (params.runtimeDeps?.length) {
for (const dependencyName of params.runtimeDeps) {
if (params.pluginLocalRuntimeDeps?.length) {
for (const dependencyName of params.pluginLocalRuntimeDeps) {
it(`keeps ${dependencyName} plugin-local`, () => {
const rootManifest = readJson<PackageManifest>("package.json");
const pluginManifest = readJson<PackageManifest>(packagePath);
const pluginSpec = pluginManifest.dependencies?.[dependencyName];
const rootSpec = rootManifest.dependencies?.[dependencyName];
const pluginSpec =
pluginManifest.dependencies?.[dependencyName] ??
pluginManifest.optionalDependencies?.[dependencyName];
const rootSpec =
rootManifest.dependencies?.[dependencyName] ??
rootManifest.optionalDependencies?.[dependencyName];
expect(pluginSpec).toBeTruthy();
expect(rootSpec).toBeUndefined();
@@ -43,6 +52,27 @@ export function describePackageManifestContract(params: PackageManifestContractP
}
}
if (params.mirroredRootRuntimeDeps?.length) {
for (const dependencyName of params.mirroredRootRuntimeDeps) {
it(`mirrors ${dependencyName} at the root package`, () => {
const rootManifest = readJson<PackageManifest>("package.json");
const pluginManifest = readJson<PackageManifest>(packagePath);
const pluginSpec =
pluginManifest.dependencies?.[dependencyName] ??
pluginManifest.optionalDependencies?.[dependencyName];
const rootSpec =
rootManifest.dependencies?.[dependencyName] ??
rootManifest.optionalDependencies?.[dependencyName];
const allowlist = pluginManifest.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
expect(pluginSpec).toBeTruthy();
expect(rootSpec).toBe(pluginSpec);
expect(Array.isArray(allowlist)).toBe(true);
expect(allowlist).toContain(dependencyName);
});
}
}
const minHostVersionBaseline = params.minHostVersionBaseline;
if (minHostVersionBaseline) {
it("declares a parseable minHostVersion floor at or above the baseline", () => {

View File

@@ -1,7 +1,12 @@
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import {
buildPublishedInstallScenarios,
collectInstalledMirroredRootDependencyManifestErrors,
collectInstalledPackageErrors,
resolveInstalledBinaryPath,
} from "../scripts/openclaw-npm-postpublish-verify.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
@@ -54,3 +59,185 @@ describe("collectInstalledPackageErrors", () => {
expect(errors.length).toBeGreaterThanOrEqual(1 + BUNDLED_RUNTIME_SIDECAR_PATHS.length);
});
});
describe("resolveInstalledBinaryPath", () => {
it("uses the Unix global bin path on non-Windows platforms", () => {
expect(resolveInstalledBinaryPath("/tmp/openclaw-prefix", "darwin")).toBe(
"/tmp/openclaw-prefix/bin/openclaw",
);
});
it("uses the Windows npm shim path on win32", () => {
expect(resolveInstalledBinaryPath("C:/openclaw-prefix", "win32")).toBe(
"C:/openclaw-prefix/openclaw.cmd",
);
});
});
describe("collectInstalledMirroredRootDependencyManifestErrors", () => {
function makeInstalledPackageRoot(): string {
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-installed-"));
}
function writePackageFile(root: string, relativePath: string, value: unknown): void {
const fullPath = join(root, relativePath);
mkdirSync(dirname(fullPath), { recursive: true });
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
it("flags missing mirrored root dependencies required by bundled extensions", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
dependencies: {},
});
writePackageFile(packageRoot, "dist/extensions/slack/package.json", {
dependencies: {
"@slack/web-api": "^7.15.0",
},
openclaw: {
releaseChecks: {
rootDependencyMirrorAllowlist: ["@slack/web-api"],
},
},
});
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package is missing mirrored root runtime dependency '@slack/web-api' required by a bundled extension.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("accepts mirrored root dependencies declared in package optionalDependencies", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
optionalDependencies: {
"@discordjs/opus": "^0.10.0",
},
});
writePackageFile(packageRoot, "dist/extensions/discord/package.json", {
optionalDependencies: {
"@discordjs/opus": "^0.10.0",
},
openclaw: {
releaseChecks: {
rootDependencyMirrorAllowlist: ["@discordjs/opus"],
},
},
});
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("flags mirrored root dependency version mismatches", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
dependencies: {
"@slack/web-api": "^7.16.0",
},
});
writePackageFile(packageRoot, "dist/extensions/slack/package.json", {
dependencies: {
"@slack/web-api": "^7.15.0",
},
openclaw: {
releaseChecks: {
rootDependencyMirrorAllowlist: ["@slack/web-api"],
},
},
});
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package mirrored dependency '@slack/web-api' version mismatch: root '^7.16.0', extension '^7.15.0'.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("flags malformed bundled extension manifests instead of silently skipping them", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
writeFileSync(
join(packageRoot, "dist/extensions/slack/package.json"),
'{\n "openclaw": { invalid json\n',
"utf8",
);
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("flags bundled extension directories that are missing package.json", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
`installed bundled extension manifest missing: ${join(packageRoot, "dist/extensions/slack/package.json")}.`,
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("rejects bundled extension manifests that are not regular files", () => {
const packageRoot = makeInstalledPackageRoot();
const outsideManifestRoot = makeInstalledPackageRoot();
try {
writePackageFile(packageRoot, "package.json", {
version: "2026.4.9",
dependencies: {},
});
writePackageFile(outsideManifestRoot, "package.json", {
dependencies: {
"@slack/web-api": "^7.15.0",
},
});
mkdirSync(join(packageRoot, "dist/extensions/slack"), { recursive: true });
symlinkSync(
join(outsideManifestRoot, "package.json"),
join(packageRoot, "dist/extensions/slack/package.json"),
);
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)).toEqual([
expect.stringContaining("installed bundled extension manifest invalid: failed to parse"),
]);
expect(collectInstalledMirroredRootDependencyManifestErrors(packageRoot)[0]).toContain(
"manifest must be a regular file",
);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
rmSync(outsideManifestRoot, { recursive: true, force: true });
}
});
});