重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -0,0 +1,149 @@
import { describe, expect, it } from "vitest";
import {
HELP_TEXT,
describeSeamKinds,
determineSeamTestStatus,
} from "../../scripts/audit-seams.mjs";
describe("audit-seams cron seam classification", () => {
it("detects cron agent handoff and outbound delivery boundaries", () => {
const source = `
import { runCliAgent } from "../../agents/cli-runner.js";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { registerAgentRunContext } from "../../infra/agent-events.js";
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
export async function runCronIsolatedAgentTurn() {
registerAgentRunContext({});
await runWithModelFallback(() => runCliAgent({}));
await deliverOutboundPayloads({ payloads: [{ text: "done" }] });
return buildOutboundSessionContext({});
}
`;
expect(describeSeamKinds("src/cron/isolated-agent/run.ts", source)).toEqual([
"cron-agent-handoff",
"cron-outbound-delivery",
]);
});
it("detects scheduler-state seams in cron service orchestration", () => {
const source = `
import { recomputeNextRuns, computeJobNextRunAtMs } from "./jobs.js";
import { ensureLoaded, persist } from "./store.js";
import { armTimer, runMissedJobs } from "./timer.js";
export async function start(state) {
await ensureLoaded(state);
recomputeNextRuns(state);
await persist(state);
armTimer(state);
await runMissedJobs(state);
return computeJobNextRunAtMs(state.store.jobs[0], Date.now());
}
`;
expect(describeSeamKinds("src/cron/service/ops.ts", source)).toContain("cron-scheduler-state");
});
});
describe("audit-seams subagent seam classification", () => {
it("detects subagent spawn and cleanup handoff boundaries", () => {
const source = `
import { callGateway } from "../gateway/call.js";
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
import { registerSubagentRun } from "./subagent-registry.js";
export async function spawnSubagentDirect() {
const response = await callGateway({ method: "agent.run", params: { task: "do it" } });
registerSubagentRun({ childSessionKey: "agent:main:subagent:child" });
await callGateway({ method: "sessions.delete", params: { key: "agent:main:subagent:child" } });
emitSessionLifecycleEvent({ sessionKey: "agent:main:subagent:child", type: "spawned" });
return response;
}
`;
expect(describeSeamKinds("src/agents/subagent-spawn.ts", source)).toEqual([
"subagent-lifecycle-registry",
"subagent-session-cleanup",
"subagent-session-spawn",
]);
});
it("detects subagent lifecycle registry and announce delivery seams", () => {
const source = `
import { resolveContextEngine } from "../context-engine/registry.js";
import { captureSubagentCompletionReply, runSubagentAnnounceFlow } from "./subagent-announce.js";
import { emitSubagentEndedHookOnce } from "./subagent-registry-completion.js";
import { persistSubagentRunsToDisk } from "./subagent-registry-state.js";
export async function completeRun(entry) {
await resolveContextEngine({});
await captureSubagentCompletionReply(entry.childSessionKey);
await emitSubagentEndedHookOnce({ runId: entry.runId });
persistSubagentRunsToDisk(new Map());
return runSubagentAnnounceFlow({ childSessionKey: entry.childSessionKey });
}
`;
expect(describeSeamKinds("src/agents/subagent-registry.ts", source)).toEqual([
"subagent-announce-delivery",
"subagent-lifecycle-registry",
]);
});
it("detects parent-stream seams for ACP spawn relays", () => {
const source = `
import { onAgentEvent } from "../infra/agent-events.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
export function startAcpSpawnParentStreamRelay() {
onAgentEvent("agent-output", () => {});
requestHeartbeatNow({ sessionKey: "agent:main" });
enqueueSystemEvent("progress", { sessionKey: "agent:main", contextKey: "stream" });
return { streamTo: "parent" };
}
`;
expect(describeSeamKinds("src/agents/acp-spawn-parent-stream.ts", source)).toEqual([
"subagent-parent-stream",
]);
});
});
describe("audit-seams status/help", () => {
it("keeps cron seam statuses conservative when nearby tests exist", () => {
expect(
determineSeamTestStatus(
["cron-agent-handoff"],
[{ file: "src/cron/service.issue-regressions.test.ts", matchQuality: "path-nearby" }],
),
).toEqual({
status: "partial",
reason:
"Nearby tests exist (best match: path-nearby), but this inventory does not prove cross-layer seam coverage end to end.",
});
});
it("keeps subagent seam statuses conservative when nearby tests exist", () => {
expect(
determineSeamTestStatus(
["subagent-session-spawn"],
[{ file: "src/agents/subagent-spawn.workspace.test.ts", matchQuality: "direct-import" }],
),
).toEqual({
status: "partial",
reason:
"Nearby tests exist (best match: direct-import), but this inventory does not prove cross-layer seam coverage end to end.",
});
});
it("documents cron and subagent seam coverage in help text", () => {
expect(HELP_TEXT).toContain("cron orchestration seams");
expect(HELP_TEXT).toContain("subagent seams");
expect(HELP_TEXT).toContain("announce delivery");
expect(HELP_TEXT).toContain("parent streaming");
});
});

View file

@ -0,0 +1,207 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
BUILD_ALL_PROFILES,
BUILD_ALL_STEPS,
resolveBuildAllStepCacheState,
resolveBuildAllStep,
resolveBuildAllSteps,
restoreBuildAllStepCacheOutputs,
writeBuildAllStepCacheStamp,
} from "../../scripts/build-all.mjs";
describe("resolveBuildAllStep", () => {
it("routes pnpm steps through the npm_execpath pnpm runner on Windows", () => {
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "canvas:a2ui:bundle");
expect(step).toBeTruthy();
const result = resolveBuildAllStep(step, {
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
env: {},
});
expect(result).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "canvas:a2ui:bundle"],
options: {
stdio: "inherit",
env: {},
shell: false,
windowsVerbatimArguments: undefined,
},
});
});
it("keeps node steps on the current node binary", () => {
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "runtime-postbuild");
expect(step).toBeTruthy();
const result = resolveBuildAllStep(step, {
nodeExecPath: "/custom/node",
env: { FOO: "bar" },
});
expect(result).toEqual({
command: "/custom/node",
args: ["scripts/runtime-postbuild.mjs"],
options: {
stdio: "inherit",
env: { FOO: "bar" },
},
});
});
it("adds heap headroom for plugin-sdk dts on Windows", () => {
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "build:plugin-sdk:dts");
expect(step).toBeTruthy();
const result = resolveBuildAllStep(step, {
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
env: { FOO: "bar" },
});
expect(result).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: ["C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs", "build:plugin-sdk:dts"],
options: {
stdio: "inherit",
env: {
FOO: "bar",
NODE_OPTIONS: "--max-old-space-size=4096",
},
shell: false,
windowsVerbatimArguments: undefined,
},
});
});
});
describe("resolveBuildAllSteps", () => {
it("keeps the full profile aligned with the declared steps", () => {
expect(resolveBuildAllSteps("full")).toEqual(BUILD_ALL_STEPS);
expect(BUILD_ALL_PROFILES.full).toEqual(BUILD_ALL_STEPS.map((step) => step.label));
});
it("uses a runtime-only profile for ci artifacts", () => {
expect(resolveBuildAllSteps("ciArtifacts").map((step) => step.label)).toEqual([
"canvas:a2ui:bundle",
"tsdown",
"runtime-postbuild",
"write-npm-update-compat-sidecars",
"build-stamp",
"canvas-a2ui-copy",
"copy-hook-metadata",
"copy-export-html-templates",
"write-build-info",
"write-cli-startup-metadata",
"write-cli-compat",
]);
});
it("rejects unknown build profiles", () => {
expect(() => resolveBuildAllSteps("wat")).toThrow("Unknown build profile: wat");
});
});
describe("resolveBuildAllStepCacheState", () => {
it("marks cacheable steps fresh when the input signature matches", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-cache-"));
try {
const inputPath = path.join(rootDir, "src/input.ts");
const outputPath = path.join(rootDir, "dist/output.js");
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(inputPath, "input");
fs.writeFileSync(outputPath, "output");
const step = {
label: "cached",
cache: {
inputs: ["src"],
outputs: ["dist"],
},
};
const cacheState = resolveBuildAllStepCacheState(step, { rootDir });
writeBuildAllStepCacheStamp(step, cacheState, { rootDir });
expect(resolveBuildAllStepCacheState(step, { rootDir })).toMatchObject({
cacheable: true,
fresh: true,
reason: "fresh",
inputFiles: 1,
outputFiles: 1,
});
} finally {
fs.rmSync(rootDir, { force: true, recursive: true });
}
});
it("marks cacheable steps stale when an input changes", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-cache-"));
try {
const inputPath = path.join(rootDir, "src/input.ts");
const outputPath = path.join(rootDir, "dist/output.js");
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(inputPath, "input");
fs.writeFileSync(outputPath, "output");
const step = {
label: "cached",
cache: {
inputs: ["src"],
outputs: ["dist"],
},
};
const cacheState = resolveBuildAllStepCacheState(step, { rootDir });
writeBuildAllStepCacheStamp(step, cacheState, { rootDir });
fs.writeFileSync(inputPath, "changed");
expect(resolveBuildAllStepCacheState(step, { rootDir })).toMatchObject({
cacheable: true,
fresh: false,
reason: "stale",
});
} finally {
fs.rmSync(rootDir, { force: true, recursive: true });
}
});
it("restores cached outputs when generated files were removed", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-build-cache-"));
try {
const inputPath = path.join(rootDir, "src/input.ts");
const outputPath = path.join(rootDir, "dist/output.js");
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(inputPath, "input");
fs.writeFileSync(outputPath, "output");
const step = {
label: "cached",
cache: {
inputs: ["src"],
outputs: ["dist"],
},
};
const cacheState = resolveBuildAllStepCacheState(step, { rootDir });
writeBuildAllStepCacheStamp(step, cacheState, { rootDir });
fs.rmSync(path.join(rootDir, "dist"), { force: true, recursive: true });
const restorable = resolveBuildAllStepCacheState(step, { rootDir });
expect(restorable).toMatchObject({
cacheable: true,
fresh: true,
reason: "fresh-cache",
restorable: true,
});
expect(restoreBuildAllStepCacheOutputs(restorable, { rootDir })).toBe(true);
expect(fs.readFileSync(outputPath, "utf8")).toBe("output");
} finally {
fs.rmSync(rootDir, { force: true, recursive: true });
}
});
});

View file

@ -0,0 +1,85 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
compareNormalizedPaths,
getBundleHashInputPaths,
getBundleHashRepoInputPaths,
getLocalRolldownCliCandidates,
getResolvedBundleDependencyPackageJsonPaths,
isBundleHashInputPath,
} from "../../scripts/bundle-a2ui.mjs";
describe("scripts/bundle-a2ui.mjs", () => {
it("keeps generated renderer output out of bundle hash inputs", () => {
const repoRoot = path.resolve("repo-root");
expect(
isBundleHashInputPath(
path.join(repoRoot, "vendor", "a2ui", "renderers", "lit", "src", "index.ts"),
repoRoot,
),
).toBe(true);
expect(
isBundleHashInputPath(
path.join(repoRoot, "vendor", "a2ui", "renderers", "lit", "dist"),
repoRoot,
),
).toBe(false);
expect(
isBundleHashInputPath(
path.join(repoRoot, "vendor", "a2ui", "renderers", "lit", "dist", "src", "index.js"),
repoRoot,
),
).toBe(false);
});
it("prefers the installed rolldown CLI over a network dlx fallback", () => {
const repoRoot = path.resolve("repo-root");
expect(getLocalRolldownCliCandidates(repoRoot)[0]).toBe(
path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"),
);
});
it("sorts hash inputs without locale-dependent collation", () => {
const paths = ["repo/Z.ts", "repo/a.ts", "repo/ä.ts", "repo/A.ts"];
expect([...paths].toSorted(compareNormalizedPaths)).toEqual([
"repo/A.ts",
"repo/Z.ts",
"repo/a.ts",
"repo/ä.ts",
]);
});
it("keeps repo-root package churn out of bundle hash inputs", () => {
const repoRoot = path.resolve("repo-root");
const inputPaths = getBundleHashRepoInputPaths(repoRoot);
expect(inputPaths).toContain(path.join(repoRoot, "ui", "package.json"));
expect(inputPaths).not.toContain(path.join(repoRoot, "package.json"));
expect(inputPaths).not.toContain(path.join(repoRoot, "pnpm-lock.yaml"));
});
it("tracks only the resolved bundle dependency manifests from node_modules", () => {
const repoRoot = process.cwd();
const dependencyPaths = getResolvedBundleDependencyPackageJsonPaths(repoRoot);
const relativeDependencyPaths = dependencyPaths.map((dependencyPath) =>
path.relative(repoRoot, dependencyPath).replaceAll(path.sep, "/"),
);
expect(
relativeDependencyPaths.map((relativePath) => relativePath.replace(/^ui\//u, "")),
).toEqual([
path.posix.join("node_modules", "lit", "package.json"),
path.posix.join("node_modules", "@lit/context", "package.json"),
path.posix.join("node_modules", "@lit-labs/signals", "package.json"),
path.posix.join("node_modules", "signal-utils", "package.json"),
]);
expect(
relativeDependencyPaths.every((relativePath) => /^(ui\/)?node_modules\//u.test(relativePath)),
).toBe(true);
expect(getBundleHashInputPaths(repoRoot)).not.toContain(path.join(repoRoot, "package.json"));
expect(getBundleHashInputPaths(repoRoot)).not.toContain(path.join(repoRoot, "pnpm-lock.yaml"));
});
});

View file

@ -0,0 +1,132 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
listBundledPluginBuildEntries,
listBundledPluginPackArtifacts,
} from "../../scripts/lib/bundled-plugin-build-entries.mjs";
describe("bundled plugin build entries", () => {
const bundledChannelEntrySources = ["index.ts", "channel-entry.ts", "setup-entry.ts"];
const forEachBundledChannelEntry = (
visit: (params: { entryPath: string; entry: string; pluginId: string }) => void,
) => {
for (const dirent of fs.readdirSync("extensions", { withFileTypes: true })) {
if (!dirent.isDirectory()) {
continue;
}
for (const sourceEntry of bundledChannelEntrySources) {
const entryPath = path.join("extensions", dirent.name, sourceEntry);
if (!fs.existsSync(entryPath)) {
continue;
}
visit({
entryPath,
entry: fs.readFileSync(entryPath, "utf8"),
pluginId: dirent.name,
});
}
}
};
it("includes manifest-less runtime core support packages in dist build entries", () => {
const entries = listBundledPluginBuildEntries();
expect(entries).toMatchObject({
"extensions/image-generation-core/api": "extensions/image-generation-core/api.ts",
"extensions/image-generation-core/runtime-api":
"extensions/image-generation-core/runtime-api.ts",
"extensions/media-understanding-core/runtime-api":
"extensions/media-understanding-core/runtime-api.ts",
"extensions/speech-core/api": "extensions/speech-core/api.ts",
"extensions/speech-core/runtime-api": "extensions/speech-core/runtime-api.ts",
});
});
it("keeps the Matrix packaged runtime shim in bundled plugin build entries", () => {
const entries = listBundledPluginBuildEntries();
expect(entries).toMatchObject({
"extensions/matrix/plugin-entry.handlers.runtime":
"extensions/matrix/plugin-entry.handlers.runtime.ts",
});
});
it("packs runtime core support packages without requiring plugin manifests", () => {
const artifacts = listBundledPluginPackArtifacts();
expect(artifacts).toContain("dist/extensions/image-generation-core/package.json");
expect(artifacts).toContain("dist/extensions/image-generation-core/runtime-api.js");
expect(artifacts).not.toContain("dist/extensions/image-generation-core/openclaw.plugin.json");
expect(artifacts).toContain("dist/extensions/media-understanding-core/runtime-api.js");
expect(artifacts).not.toContain(
"dist/extensions/media-understanding-core/openclaw.plugin.json",
);
expect(artifacts).toContain("dist/extensions/speech-core/runtime-api.js");
expect(artifacts).not.toContain("dist/extensions/speech-core/openclaw.plugin.json");
});
it("packs the Matrix packaged runtime shim", () => {
const artifacts = listBundledPluginPackArtifacts();
expect(artifacts).toContain("dist/extensions/matrix/plugin-entry.handlers.runtime.js");
});
it("keeps private QA bundles out of required npm pack artifacts", () => {
const artifacts = listBundledPluginPackArtifacts();
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/qa-channel/"))).toBe(
false,
);
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/qa-lab/"))).toBe(
false,
);
expect(artifacts.some((artifact) => artifact.startsWith("dist/extensions/qa-matrix/"))).toBe(
false,
);
});
it("keeps bundled channel secret contracts on packed top-level sidecars", () => {
const artifacts = listBundledPluginPackArtifacts();
const offenders: string[] = [];
const secretBackedPluginIds = new Set<string>();
forEachBundledChannelEntry(({ entryPath, entry, pluginId }) => {
if (!entry.includes('exportName: "channelSecrets"')) {
return;
}
secretBackedPluginIds.add(pluginId);
if (entry.includes("./src/secret-contract.js")) {
offenders.push(entryPath);
}
expect(entry).toContain('specifier: "./secret-contract-api.js"');
});
expect(offenders).toEqual([]);
for (const pluginId of [...secretBackedPluginIds].toSorted()) {
const secretApiPath = path.join("extensions", pluginId, "secret-contract-api.ts");
expect(fs.readFileSync(secretApiPath, "utf8")).toContain("channelSecrets");
expect(artifacts).toContain(`dist/extensions/${pluginId}/secret-contract-api.js`);
}
});
it("keeps bundled channel entry metadata on packed top-level sidecars", () => {
const offenders: string[] = [];
forEachBundledChannelEntry(({ entryPath, entry }) => {
if (
!entry.includes("defineBundledChannelEntry") &&
!entry.includes("defineBundledChannelSetupEntry")
) {
return;
}
if (/specifier:\s*["']\.\/src\//u.test(entry)) {
offenders.push(entryPath);
}
});
expect(offenders).toEqual([]);
});
});

View file

@ -0,0 +1,81 @@
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

@ -0,0 +1,127 @@
import { describe, expect, it } from "vitest";
import {
findChannelAgnosticBoundaryViolations,
findAcpUserFacingChannelNameViolations,
findChannelCoreReverseDependencyViolations,
findSystemMarkLiteralViolations,
} from "../../scripts/check-channel-agnostic-boundaries.mjs";
describe("check-channel-agnostic-boundaries", () => {
it("flags direct channel module imports", () => {
const source = `
import { getThreadBindingManager } from "../discord/monitor/thread-bindings.js";
const x = 1;
`;
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([
{
line: 2,
reason: 'imports channel module "../discord/monitor/thread-bindings.js"',
},
]);
});
it("flags channel config path access", () => {
const source = `
const x = cfg.channels.discord?.threadBindings?.enabled;
`;
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([
{
line: 2,
reason: 'references config path "channels.discord"',
},
]);
});
it("flags channel-literal comparisons", () => {
const source = `
if (channel === "discord") {
return true;
}
`;
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([
{
line: 2,
reason: 'compares with channel id literal (channel === "discord")',
},
]);
});
it("flags object literals with explicit channel ids", () => {
const source = `
const payload = { channel: "telegram" };
`;
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([
{
line: 2,
reason: 'assigns channel id literal to "channel" ("telegram")',
},
]);
});
it("ignores non-channel literals and unrelated text", () => {
const source = `
const msg = "discord";
const payload = { mode: "persistent" };
const x = cfg.session.threadBindings?.enabled;
`;
expect(findChannelAgnosticBoundaryViolations(source)).toEqual([]);
});
it("reverse-deps mode flags channel module re-exports", () => {
const source = `
export { resolveThreadBindingIntroText } from "../discord/monitor/thread-bindings.messages.js";
`;
expect(findChannelCoreReverseDependencyViolations(source)).toEqual([
{
line: 2,
reason: 're-exports channel module "../discord/monitor/thread-bindings.messages.js"',
},
]);
});
it("reverse-deps mode ignores channel literals when no imports are present", () => {
const source = `
const channel = "discord";
const x = cfg.channels.discord?.threadBindings?.enabled;
`;
expect(findChannelCoreReverseDependencyViolations(source)).toEqual([]);
});
it("user-facing text mode flags channel names in string literals", () => {
const source = `
const message = "Bind a Discord thread first.";
`;
expect(findAcpUserFacingChannelNameViolations(source)).toEqual([
{
line: 2,
reason: 'user-facing text references channel name ("Bind a Discord thread first.")',
},
]);
});
it("user-facing text mode ignores channel names in import specifiers", () => {
const source = `
import { x } from "../discord/monitor/thread-bindings.js";
`;
expect(findAcpUserFacingChannelNameViolations(source)).toEqual([]);
});
it("system-mark guard flags hardcoded gear literals", () => {
const source = `
const line = "⚙️ Thread bindings enabled.";
`;
expect(findSystemMarkLiteralViolations(source)).toEqual([
{
line: 2,
reason: 'hardcoded system mark literal ("⚙️ Thread bindings enabled.")',
},
]);
});
it("system-mark guard ignores module import specifiers", () => {
const source = `
import { x } from "../infra/system-message.js";
`;
expect(findSystemMarkLiteralViolations(source)).toEqual([]);
});
});

View file

@ -0,0 +1,92 @@
import { describe, expect, it } from "vitest";
import { findDynamicImportAdvisories } from "../../scripts/check-dynamic-import-warts.mjs";
describe("check-dynamic-import-warts", () => {
it("flags runtime static plus dynamic imports of the same module", () => {
const source = `
import { run } from "./runtime.js";
export async function start() {
return await import("./runtime.js");
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 4,
reason: 'runtime static + dynamic import of "./runtime.js" (static line 2)',
},
]);
});
it("ignores type-only static imports", () => {
const source = `
import { type Runtime } from "./runtime.js";
export async function start(): Promise<Runtime> {
return (await import("./runtime.js")).createRuntime();
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
it("flags repeated direct dynamic imports", () => {
const source = `
export async function one() {
return await import("./runtime.js");
}
export async function two() {
return await import("./runtime.js");
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 3,
reason: 'repeated direct dynamic import of "./runtime.js" (2 callsites: 3, 6)',
},
]);
});
it("ignores cached loader patterns", () => {
const source = `
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
function loadRuntime() {
runtimePromise ??= import("./runtime.js");
return runtimePromise;
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
it("flags direct dynamic imports inside execute paths", () => {
const source = `
export function createTool() {
return {
execute: async () => {
return await import("./runtime.js");
},
};
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([
{
line: 5,
reason:
'direct dynamic import of "./runtime.js" inside execute path; move it behind a cached loader',
},
]);
});
it("allows execute paths that call cached loaders", () => {
const source = `
let runtimePromise: Promise<typeof import("./runtime.js")> | undefined;
function loadRuntime() {
runtimePromise ??= import("./runtime.js");
return runtimePromise;
}
export function createTool() {
return {
execute: async () => await loadRuntime(),
};
}
`;
expect(findDynamicImportAdvisories(source)).toEqual([]);
});
});

View file

@ -0,0 +1,371 @@
import { EventEmitter } from "node:events";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
acquireBoundaryCheckLock,
cleanupCanaryArtifactsForExtensions,
formatBoundaryCheckSuccessSummary,
formatSlowCompileSummary,
formatSkippedCompileProgress,
formatStepFailure,
installCanaryArtifactCleanup,
isBoundaryCompileFresh,
resolveBoundaryCheckLockPath,
resolveCanaryArtifactPaths,
runNodeStepAsync,
runNodeStepsWithConcurrency,
} from "../../scripts/check-extension-package-tsc-boundary.mjs";
const tempRoots = new Set<string>();
function createTempExtensionRoot(extensionId = "demo") {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-canary-"));
tempRoots.add(rootDir);
const extensionRoot = path.join(rootDir, "extensions", extensionId);
fs.mkdirSync(extensionRoot, { recursive: true });
return { rootDir, extensionRoot };
}
function writeCanaryArtifacts(rootDir: string, extensionId = "demo") {
const { canaryPath, tsconfigPath } = resolveCanaryArtifactPaths(extensionId, rootDir);
fs.writeFileSync(canaryPath, "export {};\n", "utf8");
fs.writeFileSync(tsconfigPath, '{ "extends": "./tsconfig.json" }\n', "utf8");
return { canaryPath, tsconfigPath };
}
afterEach(() => {
for (const rootDir of tempRoots) {
fs.rmSync(rootDir, { force: true, recursive: true });
}
tempRoots.clear();
});
describe("check-extension-package-tsc-boundary", () => {
it("removes stale canary artifacts across extensions", () => {
const { rootDir } = createTempExtensionRoot();
const { canaryPath, tsconfigPath } = writeCanaryArtifacts(rootDir);
cleanupCanaryArtifactsForExtensions(["demo"], rootDir);
expect(fs.existsSync(canaryPath)).toBe(false);
expect(fs.existsSync(tsconfigPath)).toBe(false);
});
it("cleans canary artifacts again on process exit", () => {
const { rootDir } = createTempExtensionRoot();
const { canaryPath, tsconfigPath } = writeCanaryArtifacts(rootDir);
const processObject = new EventEmitter();
const teardown = installCanaryArtifactCleanup(["demo"], { processObject, rootDir });
processObject.emit("exit");
teardown();
expect(fs.existsSync(canaryPath)).toBe(false);
expect(fs.existsSync(tsconfigPath)).toBe(false);
});
it("cleans stale artifacts for every extension id passed to the cleanup hook", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-canary-"));
tempRoots.add(rootDir);
fs.mkdirSync(path.join(rootDir, "extensions", "demo-a"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "extensions", "demo-b"), { recursive: true });
const demoA = writeCanaryArtifacts(rootDir, "demo-a");
const demoB = writeCanaryArtifacts(rootDir, "demo-b");
const processObject = new EventEmitter();
const teardown = installCanaryArtifactCleanup(["demo-a", "demo-b"], {
processObject,
rootDir,
});
processObject.emit("exit");
teardown();
expect(fs.existsSync(demoA.canaryPath)).toBe(false);
expect(fs.existsSync(demoA.tsconfigPath)).toBe(false);
expect(fs.existsSync(demoB.canaryPath)).toBe(false);
expect(fs.existsSync(demoB.tsconfigPath)).toBe(false);
});
it("blocks concurrent boundary checks in the same checkout", () => {
const { rootDir } = createTempExtensionRoot();
const processObject = new EventEmitter();
const release = acquireBoundaryCheckLock({ processObject, rootDir });
let thrownError = null;
try {
acquireBoundaryCheckLock({ rootDir });
} catch (error) {
thrownError = error;
}
expect(thrownError).toMatchObject({
message: expect.stringContaining("kind: lock-contention"),
fullOutput: expect.stringContaining(
"another extension package boundary check is already running",
),
kind: "lock-contention",
});
release();
const lockPath = resolveBoundaryCheckLockPath(rootDir);
expect(fs.existsSync(lockPath)).toBe(false);
});
it("summarizes long failure output with the useful tail", () => {
const stdout = Array.from({ length: 45 }, (_, index) => `stdout ${index + 1}`).join("\n");
const stderr = Array.from({ length: 3 }, (_, index) => `stderr ${index + 1}`).join("\n");
const message = formatStepFailure("demo-plugin", {
stdout,
stderr,
kind: "timeout",
elapsedMs: 4_321,
note: "demo-plugin timed out after 5000ms",
});
const messageLines = message.split("\n");
expect(message).toContain("demo-plugin");
expect(message).toContain("[... 5 earlier lines omitted ...]");
expect(message).toContain("kind: timeout");
expect(message).toContain("elapsed: 4321ms");
expect(message).toContain("stdout 45");
expect(messageLines).not.toContain("stdout 1");
expect(message).toContain("stderr:\nstderr 1\nstderr 2\nstderr 3");
expect(message).toContain("demo-plugin timed out after 5000ms");
});
it("formats a success summary with counts and elapsed time", () => {
expect(
formatBoundaryCheckSuccessSummary({
mode: "all",
compileCount: 84,
skippedCompileCount: 13,
canaryCount: 12,
prepElapsedMs: 12_345,
compileElapsedMs: 54_321,
canaryElapsedMs: 6_789,
elapsedMs: 54_321,
}),
).toBe(
[
"extension package boundary check passed",
"mode: all",
"compiled plugins: 84",
"skipped plugins: 13",
"canary plugins: 12",
"prep elapsed: 12345ms",
"compile elapsed: 54321ms",
"canary elapsed: 6789ms",
"elapsed: 54321ms",
"",
].join("\n"),
);
});
it("omits phase timings that never ran", () => {
expect(
formatBoundaryCheckSuccessSummary({
mode: "compile",
compileCount: 97,
skippedCompileCount: 0,
canaryCount: 0,
prepElapsedMs: 12_345,
compileElapsedMs: 54_321,
canaryElapsedMs: 0,
elapsedMs: 66_666,
}),
).toBe(
[
"extension package boundary check passed",
"mode: compile",
"compiled plugins: 97",
"canary plugins: 0",
"prep elapsed: 12345ms",
"compile elapsed: 54321ms",
"elapsed: 66666ms",
"",
].join("\n"),
);
});
it("formats skipped compile progress concisely", () => {
expect(
formatSkippedCompileProgress({
skippedCount: 13,
totalCount: 97,
}),
).toBe("skipped 13 fresh plugin compiles before running 84 stale plugin checks\n");
expect(
formatSkippedCompileProgress({
skippedCount: 97,
totalCount: 97,
}),
).toBe("skipped 97 fresh plugin compiles\n");
});
it("formats the slowest plugin compiles in descending order", () => {
expect(
formatSlowCompileSummary({
compileTimings: [
{ extensionId: "quick", elapsedMs: 40 },
{ extensionId: "slow", elapsedMs: 900 },
{ extensionId: "medium", elapsedMs: 250 },
],
limit: 2,
}),
).toBe(["slowest plugin compiles:", "- slow: 900ms", "- medium: 250ms", ""].join("\n"));
});
it("treats a plugin compile as fresh only when its outputs are newer than plugin and shared sdk inputs", () => {
const { rootDir, extensionRoot } = createTempExtensionRoot();
const extensionSourcePath = path.join(extensionRoot, "index.ts");
const extensionTsconfigPath = path.join(extensionRoot, "tsconfig.json");
const stampPath = path.join(extensionRoot, "dist", ".boundary-tsc.stamp");
const rootSdkTypePath = path.join(rootDir, "dist", "plugin-sdk", "core.d.ts");
const packageSdkTypePath = path.join(
rootDir,
"packages",
"plugin-sdk",
"dist",
"src",
"plugin-sdk",
"core.d.ts",
);
fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true });
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
fs.mkdirSync(path.dirname(rootSdkTypePath), { recursive: true });
fs.mkdirSync(path.dirname(packageSdkTypePath), { recursive: true });
fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8");
fs.writeFileSync(
extensionTsconfigPath,
'{ "extends": "../tsconfig.package-boundary.base.json" }\n',
"utf8",
);
fs.writeFileSync(stampPath, "ok\n", "utf8");
fs.writeFileSync(rootSdkTypePath, "export {};\n", "utf8");
fs.writeFileSync(packageSdkTypePath, "export {};\n", "utf8");
fs.utimesSync(extensionSourcePath, new Date(1_000), new Date(1_000));
fs.utimesSync(extensionTsconfigPath, new Date(1_000), new Date(1_000));
fs.utimesSync(rootSdkTypePath, new Date(500), new Date(500));
fs.utimesSync(packageSdkTypePath, new Date(2_000), new Date(2_000));
fs.utimesSync(stampPath, new Date(3_000), new Date(3_000));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
fs.utimesSync(rootSdkTypePath, new Date(500), new Date(500));
fs.utimesSync(packageSdkTypePath, new Date(500), new Date(500));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
fs.utimesSync(rootSdkTypePath, new Date(4_000), new Date(4_000));
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(false);
});
it("accepts cached input mtimes for freshness checks", () => {
const { rootDir, extensionRoot } = createTempExtensionRoot();
const extensionSourcePath = path.join(extensionRoot, "index.ts");
const stampPath = path.join(extensionRoot, "dist", ".boundary-tsc.stamp");
fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true });
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8");
fs.writeFileSync(stampPath, "ok\n", "utf8");
fs.utimesSync(extensionSourcePath, new Date(1_000), new Date(1_000));
fs.utimesSync(stampPath, new Date(3_000), new Date(3_000));
expect(
isBoundaryCompileFresh("demo", {
rootDir,
extensionNewestInputMtimeMs: 1_000,
sharedNewestInputMtimeMs: 2_000,
}),
).toBe(true);
expect(
isBoundaryCompileFresh("demo", {
rootDir,
extensionNewestInputMtimeMs: 1_000,
sharedNewestInputMtimeMs: 4_000,
}),
).toBe(false);
});
it("keeps full failure output on the thrown error for canary detection", async () => {
await expect(
runNodeStepAsync(
"demo-plugin",
[
"--eval",
[
"console.log('src/plugins/contracts/rootdir-boundary-canary.ts');",
"for (let index = 1; index <= 45; index += 1) console.log(`stdout ${index}`);",
"console.error('TS6059');",
"process.exit(2);",
].join(" "),
],
20_000,
),
).rejects.toMatchObject({
message: expect.stringContaining("[... 6 earlier lines omitted ...]"),
fullOutput: expect.stringContaining("src/plugins/contracts/rootdir-boundary-canary.ts"),
kind: "nonzero-exit",
elapsedMs: expect.any(Number),
});
}, 30_000);
it("aborts concurrent sibling steps after the first failure", async () => {
const startedAt = Date.now();
const slowStepTimeoutMs = 60_000;
const abortBudgetMs = 30_000;
await expect(
runNodeStepsWithConcurrency(
[
{
label: "fail-fast",
args: ["--eval", "process.exit(2)"],
timeoutMs: slowStepTimeoutMs,
},
{
label: "slow-step",
args: ["--eval", "setTimeout(() => {}, 60_000)"],
timeoutMs: slowStepTimeoutMs,
},
],
2,
),
).rejects.toThrow("fail-fast");
expect(Date.now() - startedAt).toBeLessThan(abortBudgetMs);
}, 45_000);
it("passes successful step timing metadata to onSuccess handlers", async () => {
const elapsedTimes: number[] = [];
await runNodeStepsWithConcurrency(
[
{
label: "demo-step",
args: ["--eval", "process.exit(0)"],
timeoutMs: 20_000,
onSuccess(result: { elapsedMs: number }) {
elapsedTimes.push(result.elapsedMs);
},
},
],
1,
);
expect(elapsedTimes).toHaveLength(1);
expect(elapsedTimes[0]).toBeGreaterThanOrEqual(0);
}, 30_000);
});

View file

@ -0,0 +1,65 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectFilesSync,
isCodeFile,
relativeToCwd,
toPosixPath,
} from "../../scripts/check-file-utils.js";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
describe("scripts/check-file-utils isCodeFile", () => {
it("accepts source files and skips declarations", () => {
expect(isCodeFile("example.ts")).toBe(true);
expect(isCodeFile("example.mjs")).toBe(true);
expect(isCodeFile("example.d.ts")).toBe(false);
});
});
describe("scripts/check-file-utils collectFilesSync", () => {
it("collects matching files while skipping common generated dirs", () => {
const rootDir = createTempDir("openclaw-check-file-utils-");
fs.mkdirSync(path.join(rootDir, "src", "nested"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "dist"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "docs", ".generated"), { recursive: true });
fs.writeFileSync(path.join(rootDir, "src", "keep.ts"), "");
fs.writeFileSync(path.join(rootDir, "src", "nested", "keep.test.ts"), "");
fs.writeFileSync(path.join(rootDir, "dist", "skip.ts"), "");
fs.writeFileSync(path.join(rootDir, "docs", ".generated", "skip.ts"), "");
const files = collectFilesSync(rootDir, {
includeFile: (filePath) => filePath.endsWith(".ts"),
}).map((filePath) => toPosixPath(path.relative(rootDir, filePath)));
expect(files.toSorted((left, right) => left.localeCompare(right))).toEqual([
"src/keep.ts",
"src/nested/keep.test.ts",
]);
});
it("supports custom skipped directories", () => {
const rootDir = createTempDir("openclaw-check-file-utils-");
fs.mkdirSync(path.join(rootDir, "fixtures"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "src"), { recursive: true });
fs.writeFileSync(path.join(rootDir, "fixtures", "skip.ts"), "");
fs.writeFileSync(path.join(rootDir, "src", "keep.ts"), "");
const files = collectFilesSync(rootDir, {
includeFile: (filePath) => filePath.endsWith(".ts"),
skipDirNames: new Set(["fixtures"]),
}).map((filePath) => toPosixPath(path.relative(rootDir, filePath)));
expect(files).toEqual(["src/keep.ts"]);
});
});
describe("scripts/check-file-utils relativeToCwd", () => {
it("renders repo-relative paths when possible", () => {
expect(relativeToCwd(path.join(process.cwd(), "scripts", "check-file-utils.ts"))).toBe(
"scripts/check-file-utils.ts",
);
});
});

View file

@ -0,0 +1,91 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
findConflictMarkerLines,
findConflictMarkersInFiles,
listTrackedFiles,
} from "../../scripts/check-no-conflict-markers.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
function git(cwd: string, ...args: string[]): string {
return execFileSync("git", args, {
cwd,
encoding: "utf8",
}).trim();
}
describe("check-no-conflict-markers", () => {
it("finds git conflict markers at the start of lines", () => {
expect(
findConflictMarkerLines(
[
"const ok = true;",
"<<<<<<< HEAD",
"value = left;",
"=======",
"value = right;",
">>>>>>> main",
].join("\n"),
),
).toEqual([2, 4, 6]);
});
it("ignores marker-like text when it is indented or inline", () => {
expect(
findConflictMarkerLines(
["Example:", " <<<<<<< HEAD", "const text = '======= not a conflict';"].join("\n"),
),
).toEqual([]);
});
it("scans text files and skips binary files", () => {
const rootDir = createTempDir("openclaw-conflict-markers-");
const textFile = path.join(rootDir, "CHANGELOG.md");
const binaryFile = path.join(rootDir, "image.png");
fs.writeFileSync(textFile, "<<<<<<< HEAD\nconflict\n>>>>>>> main\n");
fs.writeFileSync(binaryFile, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00]));
const violations = findConflictMarkersInFiles([textFile, binaryFile]);
expect(violations).toEqual([
{
filePath: textFile,
lines: [1, 3],
},
]);
});
it("finds conflict markers in tracked script files", () => {
const rootDir = createTempDir("openclaw-conflict-markers-");
git(rootDir, "init", "-q");
git(rootDir, "config", "user.email", "test@example.com");
git(rootDir, "config", "user.name", "Test User");
const scriptFile = path.join(rootDir, "scripts", "bundled-plugin-metadata-runtime.mjs");
fs.mkdirSync(path.dirname(scriptFile), { recursive: true });
fs.writeFileSync(
scriptFile,
[
"<<<<<<< HEAD",
'const left = "left";',
"=======",
'const right = "right";',
">>>>>>> branch",
].join("\n"),
);
git(rootDir, "add", "scripts/bundled-plugin-metadata-runtime.mjs");
const violations = findConflictMarkersInFiles(listTrackedFiles(rootDir));
expect(violations).toEqual([
{
filePath: scriptFile,
lines: [1, 3, 5],
},
]);
});
});

View file

@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import {
findMessagingTmpdirCallLines,
messagingTmpdirGuardSourceRoots,
} from "../../scripts/check-no-random-messaging-tmp.mjs";
describe("check-no-random-messaging-tmp", () => {
it("finds os.tmpdir calls imported from node:os", () => {
const source = `
import os from "node:os";
const dir = os.tmpdir();
`;
expect(findMessagingTmpdirCallLines(source)).toEqual([3]);
});
it("finds tmpdir named import calls from node:os", () => {
const source = `
import { tmpdir } from "node:os";
const dir = tmpdir();
`;
expect(findMessagingTmpdirCallLines(source)).toEqual([3]);
});
it("finds tmpdir calls imported from os", () => {
const source = `
import os from "os";
const dir = os.tmpdir();
`;
expect(findMessagingTmpdirCallLines(source)).toEqual([3]);
});
it("ignores mentions in comments and strings", () => {
const source = `
// os.tmpdir()
const text = "tmpdir()";
`;
expect(findMessagingTmpdirCallLines(source)).toEqual([]);
});
it("ignores tmpdir symbols that are not imported from node:os", () => {
const source = `
const tmpdir = () => "/tmp";
const dir = tmpdir();
`;
expect(findMessagingTmpdirCallLines(source)).toEqual([]);
});
it("guards src/media against host tmpdir usage", () => {
expect(messagingTmpdirGuardSourceRoots).toContain("src/media");
});
});

View file

@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { findRawWindowOpenLines } from "../../scripts/check-no-raw-window-open.mjs";
describe("check-no-raw-window-open", () => {
it("finds direct window.open calls", () => {
const source = `
function openDocs() {
window.open("https://docs.openclaw.ai");
}
`;
expect(findRawWindowOpenLines(source)).toEqual([3]);
});
it("finds globalThis.open calls", () => {
const source = `
function openDocs() {
globalThis.open("https://docs.openclaw.ai");
}
`;
expect(findRawWindowOpenLines(source)).toEqual([3]);
});
it("ignores mentions in strings and comments", () => {
const source = `
// window.open("https://example.com")
const text = "window.open('https://example.com')";
`;
expect(findRawWindowOpenLines(source)).toEqual([]);
});
it("handles parenthesized and asserted window references", () => {
const source = `
const openRef = (window as Window).open;
openRef("https://example.com");
(window as Window).open("https://example.com");
`;
expect(findRawWindowOpenLines(source)).toEqual([4]);
});
});

View file

@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import { createNodeTestShards } from "../../scripts/lib/ci-node-test-plan.mjs";
describe("scripts/lib/ci-node-test-plan.mjs", () => {
it("names the node shard checks as core test lanes", () => {
const shards = createNodeTestShards();
expect(shards).not.toHaveLength(0);
expect(shards.map((shard) => shard.checkName)).toEqual(
shards.map((shard) =>
shard.shardName.startsWith("core-unit-")
? `checks-node-core-${shard.shardName.slice("core-unit-".length)}`
: `checks-node-${shard.shardName}`,
),
);
});
it("keeps extension, bundled, contracts, and channels configs out of the core node lane", () => {
const configs = createNodeTestShards().flatMap((shard) => shard.configs);
expect(configs).not.toContain("test/vitest/vitest.channels.config.ts");
expect(configs).not.toContain("test/vitest/vitest.contracts.config.ts");
expect(configs).not.toContain("test/vitest/vitest.bundled.config.ts");
expect(configs).not.toContain("test/vitest/vitest.full-extensions.config.ts");
expect(configs).not.toContain("test/vitest/vitest.extension-telegram.config.ts");
});
it("marks only dist-dependent shards for built artifact restore", () => {
const requiresDistShardNames = createNodeTestShards()
.filter((shard) => shard.requiresDist)
.map((shard) => shard.shardName);
expect(requiresDistShardNames).toEqual([
"core-support-boundary",
"core-runtime",
"agentic-agents-plugins",
]);
});
it("splits the agentic lane into control-plane, commands, and agent/plugin shards", () => {
const shards = createNodeTestShards();
const controlPlaneShard = shards.find((shard) => shard.shardName === "agentic-control-plane");
const commandsShard = shards.find((shard) => shard.shardName === "agentic-commands");
const agentPluginShard = shards.find((shard) => shard.shardName === "agentic-agents-plugins");
expect(controlPlaneShard).toEqual({
checkName: "checks-node-agentic-control-plane",
shardName: "agentic-control-plane",
configs: [
"test/vitest/vitest.gateway-core.config.ts",
"test/vitest/vitest.gateway-client.config.ts",
"test/vitest/vitest.gateway-methods.config.ts",
"test/vitest/vitest.gateway-server.config.ts",
"test/vitest/vitest.daemon.config.ts",
],
requiresDist: false,
});
expect(commandsShard).toEqual({
checkName: "checks-node-agentic-commands",
shardName: "agentic-commands",
configs: [
"test/vitest/vitest.cli.config.ts",
"test/vitest/vitest.commands-light.config.ts",
"test/vitest/vitest.commands.config.ts",
],
requiresDist: false,
});
expect(agentPluginShard).toEqual({
checkName: "checks-node-agentic-agents-plugins",
shardName: "agentic-agents-plugins",
configs: [
"test/vitest/vitest.agents.config.ts",
"test/vitest/vitest.plugin-sdk-light.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",
"test/vitest/vitest.plugins.config.ts",
],
requiresDist: true,
});
});
});

View file

@ -0,0 +1,199 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createScriptTestHarness } from "./test-helpers.js";
const scriptPath = path.join(process.cwd(), "scripts", "committer");
const { createTempDir } = createScriptTestHarness();
function run(cwd: string, command: string, args: string[]) {
return execFileSync(command, args, {
cwd,
encoding: "utf8",
}).trim();
}
function git(cwd: string, ...args: string[]) {
return run(cwd, "git", args);
}
function createRepo() {
const repo = createTempDir("committer-test-");
git(repo, "init", "-q");
git(repo, "config", "user.email", "test@example.com");
git(repo, "config", "user.name", "Test User");
writeFileSync(path.join(repo, "seed.txt"), "seed\n");
git(repo, "add", "seed.txt");
git(repo, "commit", "-qm", "seed");
return repo;
}
function writeRepoFile(repo: string, relativePath: string, contents: string) {
const fullPath = path.join(repo, relativePath);
mkdirSync(path.dirname(fullPath), { recursive: true });
writeFileSync(fullPath, contents);
}
function installHook(repo: string, relativePath: string, contents: string) {
const fullPath = path.join(repo, relativePath);
mkdirSync(path.dirname(fullPath), { recursive: true });
writeFileSync(fullPath, contents, {
encoding: "utf8",
mode: 0o755,
});
git(repo, "config", "core.hooksPath", path.dirname(relativePath));
}
function commitWithHelper(repo: string, commitMessage: string, ...args: string[]) {
return run(repo, "bash", [scriptPath, commitMessage, ...args]);
}
function commitWithHelperArgs(repo: string, ...args: string[]) {
return run(repo, "bash", [scriptPath, ...args]);
}
function committedPaths(repo: string) {
const output = git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD");
return output.split("\n").filter(Boolean).toSorted();
}
function committedFileContents(repo: string, relativePath: string) {
return git(repo, "show", `HEAD:${relativePath}`);
}
describe("scripts/committer", () => {
it("accepts supported path argument shapes", () => {
const cases = [
{
commitMessage: "test: plain argv",
files: [
["alpha.txt", "alpha\n"],
["nested/file with spaces.txt", "beta\n"],
] as const,
args: ["alpha.txt", "nested/file with spaces.txt"],
expected: ["alpha.txt", "nested/file with spaces.txt"],
},
{
commitMessage: "test: space blob",
files: [
["alpha.txt", "alpha\n"],
["beta.txt", "beta\n"],
] as const,
args: ["alpha.txt beta.txt"],
expected: ["alpha.txt", "beta.txt"],
},
{
commitMessage: "test: newline blob",
files: [
["alpha.txt", "alpha\n"],
["nested/file with spaces.txt", "beta\n"],
] as const,
args: ["alpha.txt\nnested/file with spaces.txt"],
expected: ["alpha.txt", "nested/file with spaces.txt"],
},
] as const;
for (const testCase of cases) {
const repo = createRepo();
for (const [file, contents] of testCase.files) {
writeRepoFile(repo, file, contents);
}
commitWithHelper(repo, testCase.commitMessage, ...testCase.args);
expect(committedPaths(repo)).toEqual(testCase.expected);
}
});
it("commits changelog-only changes without pulling in unrelated dirty files", () => {
const repo = createRepo();
writeRepoFile(repo, "CHANGELOG.md", "initial\n");
writeRepoFile(repo, "unrelated.ts", "export const ok = true;\n");
git(repo, "add", "CHANGELOG.md", "unrelated.ts");
git(repo, "commit", "-qm", "seed extra files");
writeRepoFile(repo, "CHANGELOG.md", "breaking note\n");
writeRepoFile(repo, "unrelated.ts", "<<<<<<< HEAD\nleft\n=======\nright\n>>>>>>> branch\n");
commitWithHelper(repo, "docs(changelog): note breaking change", "CHANGELOG.md");
expect(committedPaths(repo)).toEqual(["CHANGELOG.md"]);
expect(git(repo, "status", "--short")).toContain("M unrelated.ts");
});
it("supports --fast before the commit message", () => {
const repo = createRepo();
writeRepoFile(repo, "note.txt", "hello\n");
const output = commitWithHelperArgs(repo, "--fast", "test: fast helper", "note.txt");
expect(output).toContain('Committed "test: fast helper" with 1 files');
expect(committedPaths(repo)).toEqual(["note.txt"]);
});
it("supports combining --force and --fast", () => {
const repo = createRepo();
writeRepoFile(repo, "note.txt", "hello\n");
const output = commitWithHelperArgs(
repo,
"--force",
"--fast",
"test: fast forced helper",
"note.txt",
);
expect(output).toContain('Committed "test: fast forced helper" with 1 files');
expect(committedPaths(repo)).toEqual(["note.txt"]);
});
it("passes FAST_COMMIT through to git hooks when using --fast", () => {
const repo = createRepo();
installHook(
repo,
".githooks/pre-commit",
'#!/usr/bin/env bash\nset -euo pipefail\n[ "${FAST_COMMIT:-}" = "1" ] || exit 91\n',
);
writeRepoFile(repo, "note.txt", "hello\n");
const output = commitWithHelperArgs(repo, "--fast", "test: fast hook env", "note.txt");
expect(output).toContain('Committed "test: fast hook env" with 1 files');
expect(committedPaths(repo)).toEqual(["note.txt"]);
});
it("commits the hook-restaged file contents and leaves the tree clean", () => {
const repo = createRepo();
installHook(
repo,
".githooks/pre-commit",
[
"#!/usr/bin/env bash",
"set -euo pipefail",
"printf 'formatted\\n' > note.txt",
"git add note.txt",
].join("\n") + "\n",
);
writeRepoFile(repo, "note.txt", "raw\n");
const output = commitWithHelperArgs(repo, "test: hook rewrite", "note.txt");
expect(output).toContain('Committed "test: hook rewrite" with 1 files');
expect(committedPaths(repo)).toEqual(["note.txt"]);
expect(committedFileContents(repo, "note.txt")).toBe("formatted");
expect(git(repo, "status", "--short", "--untracked-files=no")).toBe("");
});
it("prints usage for --help", () => {
const repo = createRepo();
const output = commitWithHelperArgs(repo, "--help");
expect(output).toContain(
'Usage: committer [--force] [--fast] "commit message" "file" ["file" ...]',
);
});
});

View file

@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { classifyBundledExtensionSourcePath } from "../../scripts/lib/extension-source-classifier.mjs";
describe("classifyBundledExtensionSourcePath", () => {
it("treats runtime barrels as non-production source", () => {
expect(classifyBundledExtensionSourcePath("extensions/msteams/runtime-api.ts")).toMatchObject({
isCodeFile: true,
isRuntimeApiBarrel: true,
isTestLike: false,
isProductionSource: false,
});
});
it("treats extension tests and fixtures as test-like across naming styles", () => {
expect(
classifyBundledExtensionSourcePath("extensions/feishu/src/monitor-handler.test.ts"),
).toMatchObject({
isTestLike: true,
isProductionSource: false,
});
expect(
classifyBundledExtensionSourcePath("extensions/discord/src/test-fixtures/message.ts"),
).toMatchObject({
isTestLike: true,
isProductionSource: false,
});
expect(
classifyBundledExtensionSourcePath("extensions/telegram/src/bot.test-harness.ts"),
).toMatchObject({
isTestLike: true,
isProductionSource: false,
});
expect(
classifyBundledExtensionSourcePath("extensions/telegram/src/target-writeback.test-shared.ts"),
).toMatchObject({
isTestLike: true,
isProductionSource: false,
});
});
it("keeps normal extension production files eligible for guardrails", () => {
expect(classifyBundledExtensionSourcePath("extensions/msteams/src/send.ts")).toMatchObject({
isCodeFile: true,
isRuntimeApiBarrel: false,
isTestLike: false,
isProductionSource: true,
});
});
});

View file

@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import {
buildReadPermissions,
normalizeRepo,
parsePermissionKeys,
parseRepoArg,
} from "../../scripts/gh-read.js";
describe("gh-read helpers", () => {
it("finds repo from gh args", () => {
expect(parseRepoArg(["pr", "view", "42", "-R", "openclaw/openclaw"])).toBe("openclaw/openclaw");
expect(parseRepoArg(["run", "list", "--repo=openclaw/docs"])).toBe("openclaw/docs");
expect(parseRepoArg(["pr", "view", "42"])).toBeNull();
});
it("normalizes repo strings from common git formats", () => {
expect(normalizeRepo("openclaw/openclaw")).toBe("openclaw/openclaw");
expect(normalizeRepo("github.com/openclaw/openclaw")).toBe("openclaw/openclaw");
expect(normalizeRepo("https://github.com/openclaw/openclaw.git")).toBe("openclaw/openclaw");
expect(normalizeRepo("git@github.com:openclaw/openclaw.git")).toBe("openclaw/openclaw");
expect(normalizeRepo("invalid")).toBeNull();
});
it("builds a read-only permission subset from granted permissions", () => {
expect(
buildReadPermissions(
{
actions: "write",
issues: "read",
administration: "write",
metadata: "read",
statuses: null,
},
["actions", "issues", "metadata", "statuses", "administration"],
),
).toEqual({
administration: "read",
actions: "read",
issues: "read",
metadata: "read",
});
});
it("parses permission key overrides", () => {
expect(parsePermissionKeys(undefined)).toContain("pull_requests");
expect(parsePermissionKeys("actions, contents ,issues")).toEqual([
"actions",
"contents",
"issues",
]);
});
});

View file

@ -0,0 +1,112 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { pinIosVersion, parseArgs } from "../../scripts/ios-pin-version.ts";
import { resolveIosVersion } from "../../scripts/lib/ios-version.ts";
import { installIosFixtureCleanup, writeIosFixture } from "./ios-version.test-support.ts";
installIosFixtureCleanup();
describe("parseArgs", () => {
it("requires exactly one pin source", () => {
expect(() => parseArgs([])).toThrow(
"Choose exactly one of --from-gateway or --version <YYYY.M.D>",
);
expect(() => parseArgs(["--from-gateway", "--version", "2026.4.7"])).toThrow(
"Choose exactly one of --from-gateway or --version <YYYY.M.D>",
);
});
});
describe("pinIosVersion", () => {
it("pins an explicit iOS release version and syncs generated artifacts", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
changelog: `# OpenClaw iOS Changelog
## Unreleased
- Draft release notes.
`,
prefix: "openclaw-ios-pin-",
});
const result = pinIosVersion({
explicitVersion: "2026.4.7",
fromGateway: false,
rootDir,
sync: true,
});
expect(result.previousVersion).toBe("2026.4.6");
expect(result.nextVersion).toBe("2026.4.7");
expect(result.packageVersion).toBeNull();
expect(resolveIosVersion(rootDir).canonicalVersion).toBe("2026.4.7");
expect(fs.readFileSync(path.join(rootDir, "apps", "ios", "version.json"), "utf8")).toContain(
'"version": "2026.4.7"',
);
expect(
fs.readFileSync(path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"), "utf8"),
).toContain("OPENCLAW_MARKETING_VERSION = 2026.4.7");
expect(
fs.readFileSync(
path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US", "release_notes.txt"),
"utf8",
),
).toContain("- Draft release notes.");
expect(result.syncedPaths).toHaveLength(2);
});
it("pins from the current gateway version without carrying prerelease suffixes", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
packageVersion: "2026.4.10-beta.3",
changelog: `# OpenClaw iOS Changelog
## Unreleased
- Candidate release notes.
`,
prefix: "openclaw-ios-pin-",
});
const result = pinIosVersion({
explicitVersion: null,
fromGateway: true,
rootDir,
sync: true,
});
expect(result.previousVersion).toBe("2026.4.6");
expect(result.nextVersion).toBe("2026.4.10");
expect(result.packageVersion).toBe("2026.4.10-beta.3");
expect(resolveIosVersion(rootDir).marketingVersion).toBe("2026.4.10");
});
it("can skip syncing checked-in artifacts when requested", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
changelog: `# OpenClaw iOS Changelog
## Unreleased
- Candidate release notes.
`,
versionXcconfig: "stale\n",
releaseNotes: "stale\n",
prefix: "openclaw-ios-pin-",
});
const result = pinIosVersion({
explicitVersion: "2026.4.8",
fromGateway: false,
rootDir,
sync: false,
});
expect(result.syncedPaths).toHaveLength(0);
expect(
fs.readFileSync(path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"), "utf8"),
).toBe("stale\n");
});
});

View file

@ -0,0 +1,236 @@
import { execFileSync } from "node:child_process";
import { chmodSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash";
const BASH_ARGS = process.platform === "win32" ? [SCRIPT] : ["--noprofile", "--norc", SCRIPT];
const BASE_PATH = process.env.PATH ?? "/usr/bin:/bin";
const BASE_LANG = process.env.LANG ?? "C";
let fixtureRoot = "";
let sharedBinDir = "";
let sharedHomeDir = "";
let sharedHomeBinDir = "";
let sharedFakePythonPath = "";
const tempDirs: string[] = [];
const runScriptCache = new Map<string, { ok: boolean; stdout: string; stderr: string }>();
type TeamCandidate = {
teamId: string;
isFree: boolean;
teamName: string;
};
function parseTeamCandidateRows(raw: string): TeamCandidate[] {
return raw
.split("\n")
.map((line) => line.replace(/\r/g, "").trim())
.filter(Boolean)
.map((line) => line.split("\t"))
.filter((parts) => parts.length >= 3)
.map((parts) => ({
teamId: parts[0] ?? "",
isFree: (parts[1] ?? "0") === "1",
teamName: parts[2] ?? "",
}))
.filter((candidate) => candidate.teamId.length > 0);
}
function pickTeamIdFromCandidates(params: {
candidates: TeamCandidate[];
preferredTeamId?: string;
preferredTeamName?: string;
preferNonFreeTeam?: boolean;
}): string | undefined {
const preferredTeamId = (params.preferredTeamId ?? "").trim();
if (preferredTeamId) {
const preferred = params.candidates.find((candidate) => candidate.teamId === preferredTeamId);
if (preferred) {
return preferred.teamId;
}
}
const preferredTeamName = (params.preferredTeamName ?? "").trim().toLowerCase();
if (preferredTeamName) {
const preferredByName = params.candidates.find(
(candidate) => candidate.teamName.trim().toLowerCase() === preferredTeamName,
);
if (preferredByName) {
return preferredByName.teamId;
}
}
if (params.preferNonFreeTeam !== false) {
const paid = params.candidates.find((candidate) => !candidate.isFree);
if (paid) {
return paid.teamId;
}
}
return params.candidates[0]?.teamId;
}
async function writeExecutable(filePath: string, body: string): Promise<void> {
await writeFile(filePath, body, "utf8");
chmodSync(filePath, 0o755);
}
function runScript(
homeDir: string,
extraEnv: Record<string, string> = {},
): {
ok: boolean;
stdout: string;
stderr: string;
} {
const extraEnvKey = Object.keys(extraEnv)
.toSorted((a, b) => a.localeCompare(b))
.map((key) => `${key}=${extraEnv[key] ?? ""}`)
.join("\u0001");
const cacheKey = `${homeDir}\u0000${extraEnvKey}`;
const cached = runScriptCache.get(cacheKey);
if (cached) {
return cached;
}
const binDir = path.join(homeDir, "bin");
const env = {
HOME: homeDir,
PATH: `${binDir}${path.delimiter}${sharedBinDir}${path.delimiter}${BASE_PATH}`,
LANG: BASE_LANG,
...extraEnv,
};
try {
const stdout = execFileSync(BASH_BIN, BASH_ARGS, {
env,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
const result = { ok: true, stdout: stdout.trim(), stderr: "" };
runScriptCache.set(cacheKey, result);
return result;
} catch (error) {
const e = error as { stdout?: unknown; stderr?: unknown };
const stdout =
typeof e.stdout === "string"
? e.stdout
: Buffer.isBuffer(e.stdout)
? e.stdout.toString("utf8")
: "";
const stderr =
typeof e.stderr === "string"
? e.stderr
: Buffer.isBuffer(e.stderr)
? e.stderr.toString("utf8")
: "";
const result = { ok: false, stdout: stdout.trim(), stderr: stderr.trim() };
runScriptCache.set(cacheKey, result);
return result;
}
}
describe("scripts/ios-team-id.sh", () => {
beforeAll(async () => {
fixtureRoot = makeTempDir(tempDirs, "openclaw-ios-team-id-");
sharedBinDir = path.join(fixtureRoot, "shared-bin");
await mkdir(sharedBinDir, { recursive: true });
sharedHomeDir = path.join(fixtureRoot, "home");
sharedHomeBinDir = path.join(sharedHomeDir, "bin");
await mkdir(sharedHomeBinDir, { recursive: true });
await mkdir(path.join(sharedHomeDir, "Library", "Preferences"), { recursive: true });
await writeFile(
path.join(sharedHomeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"),
"",
);
await writeExecutable(
path.join(sharedBinDir, "plutil"),
`#!/usr/bin/env bash
echo '{}'`,
);
await writeExecutable(
path.join(sharedBinDir, "defaults"),
`#!/usr/bin/env bash
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
echo '(identifier = "dev@example.com";)'
exit 0
fi
exit 0`,
);
await writeExecutable(
path.join(sharedBinDir, "security"),
`#!/usr/bin/env bash
if [[ "$1" == "cms" && "$2" == "-D" ]]; then
if [[ "$4" == *"one.mobileprovision" ]]; then
cat <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>TeamIdentifier</key><array><string>AAAAA11111</string></array></dict></plist>
PLIST
exit 0
fi
if [[ "$4" == *"two.mobileprovision" ]]; then
cat <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict><key>TeamIdentifier</key><array><string>BBBBB22222</string></array></dict></plist>
PLIST
exit 0
fi
fi
exit 1`,
);
sharedFakePythonPath = path.join(sharedHomeBinDir, "fake-python");
await writeExecutable(
sharedFakePythonPath,
`#!/usr/bin/env bash
printf 'AAAAA11111\\t0\\tAlpha Team\\r\\n'
printf 'BBBBB22222\\t0\\tBeta Team\\r\\n'`,
);
});
afterAll(async () => {
cleanupTempDirs(tempDirs);
});
it("parses team listings and prioritizes preferred IDs without shelling out", () => {
const rows = parseTeamCandidateRows(
"AAAAA11111\t1\tAlpha Team\r\nBBBBB22222\t0\tBeta Team\r\n",
);
expect(rows).toStrictEqual([
{ teamId: "AAAAA11111", isFree: true, teamName: "Alpha Team" },
{ teamId: "BBBBB22222", isFree: false, teamName: "Beta Team" },
]);
const preferred = pickTeamIdFromCandidates({
candidates: rows,
preferredTeamId: "BBBBB22222",
});
expect(preferred).toBe("BBBBB22222");
const fallback = pickTeamIdFromCandidates({
candidates: rows,
preferredTeamId: "CCCCCC3333",
});
expect(fallback).toBe("BBBBB22222");
});
it("resolves a fallback team ID from Xcode team listings (smoke)", async () => {
const fallbackResult = runScript(sharedHomeDir, { IOS_PYTHON_BIN: sharedFakePythonPath });
expect(fallbackResult.ok).toBe(true);
expect(fallbackResult.stdout).toBe("AAAAA11111");
});
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
const result = runScript(sharedHomeDir);
expect(result.ok).toBe(false);
expect(
result.stderr.includes("An Apple account is signed in to Xcode") ||
result.stderr.includes("No Apple Team ID found in Xcode accounts"),
).toBe(true);
expect(
result.stderr.includes("IOS_DEVELOPMENT_TEAM") ||
result.stderr.includes("IOS_ALLOW_KEYCHAIN_TEAM_FALLBACK"),
).toBe(true);
});
});

View file

@ -0,0 +1,49 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const tempDirs: string[] = [];
export function installIosFixtureCleanup(): void {
afterEach(() => {
cleanupTempDirs(tempDirs);
});
}
export function writeIosFixture(params: {
version: string;
changelog: string;
packageVersion?: string;
releaseNotes?: string;
versionXcconfig?: string;
prefix?: string;
}): string {
const rootDir = makeTempDir(tempDirs, params.prefix ?? "openclaw-ios-version-");
fs.mkdirSync(path.join(rootDir, "apps", "ios", "Config"), { recursive: true });
fs.mkdirSync(path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US"), {
recursive: true,
});
fs.writeFileSync(
path.join(rootDir, "package.json"),
`${JSON.stringify({ version: params.packageVersion ?? "2026.4.6" }, null, 2)}\n`,
"utf8",
);
fs.writeFileSync(
path.join(rootDir, "apps", "ios", "version.json"),
`${JSON.stringify({ version: params.version }, null, 2)}\n`,
"utf8",
);
fs.writeFileSync(path.join(rootDir, "apps", "ios", "CHANGELOG.md"), params.changelog, "utf8");
fs.writeFileSync(
path.join(rootDir, "apps", "ios", "Config", "Version.xcconfig"),
params.versionXcconfig ?? "",
"utf8",
);
fs.writeFileSync(
path.join(rootDir, "apps", "ios", "fastlane", "metadata", "en-US", "release_notes.txt"),
params.releaseNotes ?? "",
"utf8",
);
return rootDir;
}

View file

@ -0,0 +1,138 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
extractChangelogSection,
normalizeGatewayVersionToPinnedIosVersion,
renderIosReleaseNotes,
renderIosVersionXcconfig,
resolveGatewayVersionForIosRelease,
resolveIosVersion,
} from "../../scripts/lib/ios-version.ts";
import { installIosFixtureCleanup, writeIosFixture } from "./ios-version.test-support.ts";
installIosFixtureCleanup();
describe("resolveIosVersion", () => {
it("parses pinned CalVer versions and derives Apple marketing fields", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.6\n\nStable notes.\n",
});
expect(resolveIosVersion(rootDir)).toMatchObject({
canonicalVersion: "2026.4.6",
marketingVersion: "2026.4.6",
buildVersion: "1",
});
});
it("rejects semver-only versions", () => {
const rootDir = writeIosFixture({
version: "1.2.3",
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
});
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned CalVer like 2026.4.6");
});
it("rejects prerelease suffixes in the pinned iOS version file", () => {
const rootDir = writeIosFixture({
version: "2026.4.6-beta.1",
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
});
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned CalVer like 2026.4.6");
});
});
describe("gateway version normalization", () => {
it("keeps stable gateway CalVer values", () => {
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6")).toBe("2026.4.6");
});
it("strips beta suffixes when pinning from gateway version", () => {
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-beta.2")).toBe("2026.4.6");
});
it("strips fallback correction suffixes when pinning from gateway version", () => {
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-3")).toBe("2026.4.6");
});
it("reads and normalizes the root package version for iOS releases", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
packageVersion: "2026.4.7-beta.5",
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
});
expect(resolveGatewayVersionForIosRelease(rootDir)).toEqual({
packageVersion: "2026.4.7-beta.5",
pinnedIosVersion: "2026.4.7",
});
});
});
describe("renderIosVersionXcconfig", () => {
it("renders checked-in defaults from the pinned iOS version", () => {
const rootDir = writeIosFixture({
version: "2026.4.8",
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.8\n\nNotes.\n",
});
const version = resolveIosVersion(rootDir);
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_IOS_VERSION = 2026.4.8");
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_MARKETING_VERSION = 2026.4.8");
expect(renderIosVersionXcconfig(version)).toContain("OPENCLAW_BUILD_VERSION = 1");
});
});
describe("release note extraction", () => {
it("extracts exact pinned version sections first", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
changelog: `# OpenClaw iOS Changelog
## Unreleased
Draft notes.
## 2026.4.6
- Exact release notes.
`,
});
const version = resolveIosVersion(rootDir);
const changelog = fs.readFileSync(path.join(rootDir, "apps", "ios", "CHANGELOG.md"), "utf8");
expect(renderIosReleaseNotes(version, changelog)).toBe("- Exact release notes.\n");
});
it("falls back to Unreleased when the release section does not exist yet", () => {
const rootDir = writeIosFixture({
version: "2026.4.6",
changelog: `# OpenClaw iOS Changelog
## Unreleased
### Added
- New iOS feature.
`,
});
const version = resolveIosVersion(rootDir);
const changelog = fs.readFileSync(path.join(rootDir, "apps", "ios", "CHANGELOG.md"), "utf8");
expect(renderIosReleaseNotes(version, changelog)).toContain("### Added");
expect(renderIosReleaseNotes(version, changelog)).toContain("- New iOS feature.");
});
it("extracts markdown bodies without the version heading", () => {
expect(
extractChangelogSection(
`# OpenClaw iOS Changelog\n\n## 2026.4.6 - 2026-04-06\n\nLine one.\n\n## 2026.4.5\n`,
"2026.4.6",
),
).toBe("Line one.");
});
});

View file

@ -0,0 +1,154 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const repoRoot = path.resolve(import.meta.dirname, "../..");
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
const IGNORED_DIRS = new Set([".cache", ".git", "build", "coverage", "dist", "node_modules"]);
const ROOTS = ["src", "extensions", "scripts", "ui"] as const;
const SUPPRESSION_PATTERN = /(?:oxlint|eslint)-disable(?:-next-line)?\s+([@/\w-]+)(?:\s+--|$)/u;
type SuppressionEntry = {
file: string;
rule: string;
};
function walkCodeFiles(dir: string, files: string[] = []): string[] {
if (!fs.existsSync(dir)) {
return files;
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) {
continue;
}
walkCodeFiles(fullPath, files);
continue;
}
if (!CODE_EXTENSIONS.has(path.extname(entry.name))) {
continue;
}
if (entry.name.startsWith("__rootdir_boundary_canary__.")) {
continue;
}
const relativePath = path.relative(repoRoot, fullPath).replaceAll(path.sep, "/");
if (
relativePath.includes("/test/") ||
relativePath.endsWith(".test.ts") ||
relativePath.endsWith(".test.tsx") ||
relativePath.endsWith(".spec.ts") ||
relativePath.endsWith(".spec.tsx")
) {
continue;
}
files.push(relativePath);
}
return files;
}
function collectProductionLintSuppressions(): SuppressionEntry[] {
const entries: SuppressionEntry[] = [];
const files = ROOTS.flatMap((root) => walkCodeFiles(path.join(repoRoot, root))).toSorted();
for (const relativePath of files) {
const source = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
for (const line of source.split("\n")) {
const match = line.match(SUPPRESSION_PATTERN);
if (!match) {
continue;
}
entries.push({
file: relativePath,
rule: match[1],
});
}
}
return entries;
}
function summarizeSuppressions(entries: readonly SuppressionEntry[]): string[] {
const counts = new Map<string, number>();
for (const entry of entries) {
const key = `${entry.file}|${entry.rule}`;
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return [...counts.entries()].map(([key, count]) => `${key}|${count}`).toSorted();
}
describe("production lint suppressions", () => {
it("keeps the intentional production suppression tail on an explicit allowlist", () => {
expect(summarizeSuppressions(collectProductionLintSuppressions())).toEqual([
"extensions/browser/src/browser/pw-tools-core.interactions.ts|@typescript-eslint/no-implied-eval|2",
"extensions/browser/src/cli/browser-cli-actions-input/register.files-downloads.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/browser/src/node-host/invoke-browser.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/discord/src/monitor.tool-result.test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/discord/src/outbound-adapter.test-harness.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/discord/src/test-support/provider.test-support.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/feishu/src/bitable.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/matrix/src/onboarding.test-harness.ts|typescript/no-unnecessary-type-parameters|1",
"extensions/slack/src/monitor/provider.ts|typescript/no-unnecessary-type-parameters|1",
"scripts/e2e/mcp-channels-harness.ts|unicorn/prefer-add-event-listener|1",
"scripts/lib/extension-package-boundary.ts|typescript/no-unnecessary-type-parameters|1",
"scripts/lib/plugin-npm-release.ts|typescript/no-unnecessary-type-parameters|1",
"src/agents/agent-scope.ts|no-control-regex|1",
"src/agents/pi-embedded-runner/run/images.ts|no-control-regex|1",
"src/agents/skills-clawhub.ts|no-control-regex|1",
"src/agents/subagent-attachments.ts|no-control-regex|1",
"src/agents/subagent-spawn.ts|no-control-regex|1",
"src/agents/tools/common.ts|typescript/no-explicit-any|1",
"src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1",
"src/channels/plugins/contracts/test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
"src/channels/plugins/types.plugin.ts|typescript/no-explicit-any|1",
"src/cli/cli-utils.ts|typescript/no-unnecessary-type-parameters|1",
"src/cli/command-options.ts|typescript/no-unnecessary-type-parameters|1",
"src/cli/plugins-cli-test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
"src/cli/test-runtime-capture.ts|typescript/no-unnecessary-type-parameters|1",
"src/config/types.channels.ts|@typescript-eslint/no-explicit-any|1",
"src/gateway/test-helpers.server.ts|typescript/no-unnecessary-type-parameters|1",
"src/hooks/module-loader.ts|typescript/no-unnecessary-type-parameters|1",
"src/infra/channel-runtime-context.ts|typescript/no-unnecessary-type-parameters|1",
"src/infra/exec-approvals-effective.ts|typescript/no-unnecessary-type-parameters|1",
"src/infra/json-file.ts|typescript/no-unnecessary-type-parameters|1",
"src/infra/outbound/send-deps.ts|typescript/no-unnecessary-type-parameters|1",
"src/node-host/invoke.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/channel-config-helpers.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/channel-entry-contract.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/facade-loader.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/facade-runtime.ts|typescript/no-unnecessary-type-parameters|3",
"src/plugin-sdk/qa-runner-runtime.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/hooks.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/lazy-service-module.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/runtime/runtime-channel.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/runtime/runtime-plugin-boundary.ts|typescript/no-unnecessary-type-parameters|2",
"src/plugins/runtime/types-channel.ts|typescript/no-unnecessary-type-parameters|1",
"src/tasks/task-flow-registry.store.sqlite.ts|typescript/no-unnecessary-type-parameters|1",
"src/tasks/task-registry.store.sqlite.ts|typescript/no-unnecessary-type-parameters|1",
"src/test-utils/bundled-plugin-public-surface.ts|typescript/no-unnecessary-type-parameters|2",
"src/test-utils/vitest-mock-fn.ts|typescript/no-explicit-any|1",
"src/utils.ts|typescript/no-unnecessary-type-parameters|1",
"ui/src/ui/views/overview-log-tail.ts|no-control-regex|1",
]);
});
it("keeps production no-explicit-any suppressions on an explicit allowlist", () => {
const anySuppressions = collectProductionLintSuppressions().filter(
(entry) => entry.rule === "typescript/no-explicit-any",
);
expect(anySuppressions).toEqual([
{
file: "src/agents/tools/common.ts",
rule: "typescript/no-explicit-any",
},
{
file: "src/channels/plugins/types.plugin.ts",
rule: "typescript/no-explicit-any",
},
{
file: "src/test-utils/vitest-mock-fn.ts",
rule: "typescript/no-explicit-any",
},
]);
});
});

View file

@ -0,0 +1,346 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
acquireLocalHeavyCheckLockSync,
applyLocalOxlintPolicy,
applyLocalTsgoPolicy,
shouldAcquireLocalHeavyCheckLockForOxlint,
shouldAcquireLocalHeavyCheckLockForTsgo,
} from "../../scripts/lib/local-heavy-check-runtime.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
const GIB = 1024 ** 3;
const CONSTRAINED_HOST = {
totalMemoryBytes: 16 * GIB,
logicalCpuCount: 8,
};
const ROOMY_HOST = {
totalMemoryBytes: 128 * GIB,
logicalCpuCount: 16,
};
function makeEnv(overrides: Record<string, string | undefined> = {}) {
return {
...process.env,
OPENCLAW_LOCAL_CHECK: "1",
...overrides,
};
}
describe("local-heavy-check-runtime", () => {
it("tightens local tsgo runs on constrained hosts", () => {
const { args, env } = applyLocalTsgoPolicy([], makeEnv(), CONSTRAINED_HOST);
expect(args).toEqual([
"--declaration",
"false",
"--incremental",
"--tsBuildInfoFile",
".artifacts/tsgo-cache/root.tsbuildinfo",
"--singleThreaded",
"--checkers",
"1",
]);
expect(env.GOGC).toBe("30");
expect(env.GOMEMLIMIT).toBe("3GiB");
});
it("skips declaration transforms for no-emit tsgo checks", () => {
const { args } = applyLocalTsgoPolicy([], makeEnv({ OPENCLAW_LOCAL_CHECK: "0" }), ROOMY_HOST);
expect(args).toEqual(["--declaration", "false"]);
});
it("keeps explicit tsgo flags and Go env overrides intact when throttled", () => {
const { args, env } = applyLocalTsgoPolicy(
["--checkers", "4", "--singleThreaded", "--pprofDir", "/tmp/existing"],
makeEnv({
GOGC: "80",
GOMEMLIMIT: "5GiB",
OPENCLAW_TSGO_PPROF_DIR: "/tmp/profile",
}),
CONSTRAINED_HOST,
);
expect(args).toEqual([
"--checkers",
"4",
"--singleThreaded",
"--pprofDir",
"/tmp/existing",
"--declaration",
"false",
]);
expect(env.GOGC).toBe("80");
expect(env.GOMEMLIMIT).toBe("5GiB");
});
it("keeps explicit tsgo declaration flags intact", () => {
const env = makeEnv({ OPENCLAW_LOCAL_CHECK_MODE: "full" });
const longFlag = applyLocalTsgoPolicy(["--declaration"], env, ROOMY_HOST);
const shortFlag = applyLocalTsgoPolicy(["-d"], env, ROOMY_HOST);
expect(longFlag.args).toEqual(["--declaration"]);
expect(shortFlag.args).toEqual(["-d"]);
});
it("defaults local tsgo to throttled mode on roomy hosts", () => {
const { args, env } = applyLocalTsgoPolicy([], makeEnv(), ROOMY_HOST);
expect(args).toEqual([
"--declaration",
"false",
"--incremental",
"--tsBuildInfoFile",
".artifacts/tsgo-cache/root.tsbuildinfo",
"--singleThreaded",
"--checkers",
"1",
]);
expect(env.GOGC).toBe("30");
expect(env.GOMEMLIMIT).toBe("3GiB");
});
it("uses the configured local tsgo build info file", () => {
const { args } = applyLocalTsgoPolicy(
[],
makeEnv({
OPENCLAW_LOCAL_CHECK_MODE: "full",
OPENCLAW_TSGO_BUILD_INFO_FILE: ".artifacts/custom/tsgo.tsbuildinfo",
}),
ROOMY_HOST,
);
expect(args).toEqual([
"--declaration",
"false",
"--incremental",
"--tsBuildInfoFile",
".artifacts/custom/tsgo.tsbuildinfo",
]);
});
it("avoids incremental cache reuse for ad hoc tsgo runs", () => {
const { args } = applyLocalTsgoPolicy(
["--extendedDiagnostics"],
makeEnv({ OPENCLAW_LOCAL_CHECK_MODE: "full" }),
ROOMY_HOST,
);
expect(args).toEqual(["--extendedDiagnostics", "--declaration", "false"]);
});
it("allows forcing the throttled tsgo policy on roomy hosts", () => {
const { args, env } = applyLocalTsgoPolicy(
[],
makeEnv({
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
}),
ROOMY_HOST,
);
expect(args).toEqual([
"--declaration",
"false",
"--incremental",
"--tsBuildInfoFile",
".artifacts/tsgo-cache/root.tsbuildinfo",
"--singleThreaded",
"--checkers",
"1",
]);
expect(env.GOGC).toBe("30");
expect(env.GOMEMLIMIT).toBe("3GiB");
});
it("allows forcing full-speed tsgo runs on roomy hosts", () => {
const { args, env } = applyLocalTsgoPolicy(
[],
makeEnv({
OPENCLAW_LOCAL_CHECK_MODE: "full",
}),
ROOMY_HOST,
);
expect(args).toEqual([
"--declaration",
"false",
"--incremental",
"--tsBuildInfoFile",
".artifacts/tsgo-cache/root.tsbuildinfo",
]);
expect(env.GOGC).toBeUndefined();
expect(env.GOMEMLIMIT).toBeUndefined();
});
it("skips the heavy-check lock for tsgo metadata commands", () => {
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--help"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["-h"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--version"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["-v"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--init"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--showConfig"])).toBe(false);
});
it("keeps the heavy-check lock for real tsgo runs", () => {
expect(shouldAcquireLocalHeavyCheckLockForTsgo([])).toBe(true);
expect(shouldAcquireLocalHeavyCheckLockForTsgo(["--extendedDiagnostics"])).toBe(true);
});
it("allows forcing the tsgo lock back on", () => {
expect(
shouldAcquireLocalHeavyCheckLockForTsgo(
["--help"],
makeEnv({ OPENCLAW_TSGO_FORCE_LOCK: "1" }),
),
).toBe(true);
});
it("serializes local oxlint runs onto one thread on constrained hosts", () => {
const { args } = applyLocalOxlintPolicy([], makeEnv(), CONSTRAINED_HOST);
expect(args).toEqual([
"--type-aware",
"--tsconfig",
"tsconfig.oxlint.json",
"--report-unused-disable-directives-severity",
"error",
"--threads=1",
]);
});
it("defaults local oxlint to one thread on roomy hosts", () => {
const { args } = applyLocalOxlintPolicy([], makeEnv(), ROOMY_HOST);
expect(args).toEqual([
"--type-aware",
"--tsconfig",
"tsconfig.oxlint.json",
"--report-unused-disable-directives-severity",
"error",
"--threads=1",
]);
});
it("allows forcing full-speed oxlint runs on roomy hosts", () => {
const { args } = applyLocalOxlintPolicy(
[],
makeEnv({
OPENCLAW_LOCAL_CHECK_MODE: "full",
}),
ROOMY_HOST,
);
expect(args).toEqual([
"--type-aware",
"--tsconfig",
"tsconfig.oxlint.json",
"--report-unused-disable-directives-severity",
"error",
]);
});
it("skips the heavy-check lock for explicit oxlint file targets", () => {
const cwd = createTempDir("openclaw-oxlint-lock-skip-");
const target = path.join(cwd, "sample.ts");
fs.writeFileSync(target, "export const ok = true;\n", "utf8");
expect(
shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "sample.ts"], { cwd }),
).toBe(false);
});
it("skips the heavy-check lock for oxlint metadata commands", () => {
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--help"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["-h"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--version"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["-V"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--rules"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--print-config"])).toBe(false);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--init"])).toBe(false);
});
it("keeps the heavy-check lock for directory targets and broad oxlint runs", () => {
const cwd = createTempDir("openclaw-oxlint-lock-keep-");
fs.mkdirSync(path.join(cwd, "src"), { recursive: true });
fs.writeFileSync(path.join(cwd, "src", "sample.ts"), "export const ok = true;\n", "utf8");
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "src"], { cwd })).toBe(
true,
);
expect(shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware"], { cwd })).toBe(true);
});
it("allows forcing the oxlint lock back on", () => {
const cwd = createTempDir("openclaw-oxlint-lock-force-");
fs.writeFileSync(path.join(cwd, "sample.ts"), "export const ok = true;\n", "utf8");
expect(
shouldAcquireLocalHeavyCheckLockForOxlint(["--type-aware", "--", "sample.ts"], {
cwd,
env: makeEnv({ OPENCLAW_OXLINT_FORCE_LOCK: "1" }),
}),
).toBe(true);
});
it("reclaims stale local heavy-check locks from dead pids", () => {
const cwd = createTempDir("openclaw-local-heavy-check-");
const commonDir = path.join(cwd, ".git");
const lockDir = path.join(commonDir, "openclaw-local-checks", "heavy-check.lock");
fs.mkdirSync(lockDir, { recursive: true });
fs.writeFileSync(
path.join(lockDir, "owner.json"),
`${JSON.stringify({
pid: 999_999_999,
tool: "tsgo",
cwd,
})}\n`,
"utf8",
);
const release = acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv(),
toolName: "oxlint",
});
const owner = JSON.parse(fs.readFileSync(path.join(lockDir, "owner.json"), "utf8"));
expect(owner.pid).toBe(process.pid);
expect(owner.tool).toBe("oxlint");
release();
expect(fs.existsSync(lockDir)).toBe(false);
});
it("cleans up stale legacy test locks when acquiring the shared heavy-check lock", () => {
const cwd = createTempDir("openclaw-local-heavy-check-legacy-");
const commonDir = path.join(cwd, ".git");
const locksDir = path.join(commonDir, "openclaw-local-checks");
const legacyLockDir = path.join(locksDir, "test.lock");
const heavyCheckLockDir = path.join(locksDir, "heavy-check.lock");
fs.mkdirSync(legacyLockDir, { recursive: true });
fs.writeFileSync(
path.join(legacyLockDir, "owner.json"),
`${JSON.stringify({
pid: 999_999_999,
tool: "test",
cwd,
})}\n`,
"utf8",
);
const release = acquireLocalHeavyCheckLockSync({
cwd,
env: makeEnv(),
toolName: "oxlint",
});
expect(fs.existsSync(legacyLockDir)).toBe(false);
expect(fs.existsSync(heavyCheckLockDir)).toBe(true);
release();
expect(fs.existsSync(heavyCheckLockDir)).toBe(false);
});
});

View file

@ -0,0 +1,141 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveNpmRunner } from "../../scripts/npm-runner.mjs";
describe("resolveNpmRunner", () => {
it("anchors npm staging to the active node toolchain when npm-cli.js exists", () => {
const execPath = "/Users/test/.nodenv/versions/24.13.0/bin/node";
const expectedNpmCliPath = path.posix.resolve(
path.posix.dirname(execPath),
"../lib/node_modules/npm/bin/npm-cli.js",
);
const runner = resolveNpmRunner({
execPath,
env: {},
existsSync: (candidate) => candidate === expectedNpmCliPath,
platform: "darwin",
});
expect(runner).toEqual({
command: execPath,
args: [expectedNpmCliPath],
shell: false,
});
});
it("anchors Windows npm staging to the adjacent npm-cli.js without a shell", () => {
const execPath = "C:\\nodejs\\node.exe";
const expectedNpmCliPath = path.win32.resolve(
path.win32.dirname(execPath),
"node_modules/npm/bin/npm-cli.js",
);
const runner = resolveNpmRunner({
execPath,
env: {},
existsSync: (candidate) => candidate === expectedNpmCliPath,
platform: "win32",
});
expect(runner).toEqual({
command: execPath,
args: [expectedNpmCliPath],
shell: false,
});
});
it("uses an adjacent npm.exe on Windows without a shell", () => {
const execPath = "C:\\nodejs\\node.exe";
const expectedNpmExePath = path.win32.resolve(path.win32.dirname(execPath), "npm.exe");
const runner = resolveNpmRunner({
execPath,
env: {},
existsSync: (candidate) => candidate === expectedNpmExePath,
npmArgs: ["install", "--silent"],
platform: "win32",
});
expect(runner).toEqual({
command: expectedNpmExePath,
args: ["install", "--silent"],
shell: false,
});
});
it("wraps an adjacent npm.cmd via cmd.exe without enabling shell mode", () => {
const execPath = "C:\\nodejs\\node.exe";
const npmCmdPath = path.win32.resolve(path.win32.dirname(execPath), "npm.cmd");
const runner = resolveNpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
execPath,
env: {},
existsSync: (candidate) => candidate === npmCmdPath,
npmArgs: ["install", "--omit=dev"],
platform: "win32",
});
expect(runner).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", `${npmCmdPath} install --omit=dev`],
shell: false,
windowsVerbatimArguments: true,
});
});
it("escapes caret semver specs when invoking npm.cmd through cmd.exe", () => {
const execPath = "C:\\nodejs\\node.exe";
const npmCmdPath = path.win32.resolve(path.win32.dirname(execPath), "npm.cmd");
const runner = resolveNpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
execPath,
env: {},
existsSync: (candidate) => candidate === npmCmdPath,
npmArgs: ["install", "@slack/bolt@^4.6.0"],
platform: "win32",
});
expect(runner).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", `${npmCmdPath} install @slack/bolt@^^4.6.0`],
shell: false,
windowsVerbatimArguments: true,
});
});
it("prefixes PATH with the active node dir when falling back to bare npm", () => {
expect(
resolveNpmRunner({
execPath: "/tmp/node",
env: {
PATH: "/usr/bin:/bin",
},
existsSync: () => false,
platform: "linux",
}),
).toEqual({
command: "npm",
args: [],
shell: false,
env: {
PATH: `/tmp${path.delimiter}/usr/bin:/bin`,
},
});
});
it("fails closed on Windows when no toolchain-local npm CLI exists", () => {
expect(() =>
resolveNpmRunner({
execPath: "C:\\node\\node.exe",
env: {
Path: "C:\\Windows\\System32",
},
existsSync: () => false,
platform: "win32",
}),
).toThrow("OpenClaw refuses to shell out to bare npm on Windows");
});
});

View file

@ -0,0 +1,486 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
buildWindowsDevUpdateToolchainCheckScript,
buildWindowsFreshShellVersionCheckScript,
buildWindowsPathBootstrapScript,
canConnectToLoopbackPort,
buildDiscordSmokeGuildsConfig,
buildRealUpdateEnv,
isImmutableReleaseRef,
looksLikeReleaseVersionRef,
normalizeRequestedRef,
normalizeWindowsCommandShimPath,
normalizeWindowsInstalledCliPath,
parseArgs,
packageHasScript,
readInstalledVersion,
readRunnerOverrideEnv,
resolveExplicitBaselineVersion,
resolveDevUpdateVerificationRef,
resolveInstalledPrefixDirFromCliPath,
resolvePublishedInstallerUrl,
resolveRequestedSuites,
resolveRunnerMatrix,
resolveStaticFileContentType,
shouldExerciseManagedGatewayLifecycleAfterInstall,
shouldSkipInstallerDaemonHealthCheck,
shouldStopManagedGatewayBeforeManualFallback,
shouldRunMainChannelDevUpdate,
shouldUseManagedGatewayForInstallerRuntime,
shouldUseManagedGatewayService,
verifyDevUpdateStatus,
} from "../../scripts/openclaw-cross-os-release-checks.ts";
describe("scripts/openclaw-cross-os-release-checks", () => {
it("treats explicit empty-string args as values instead of boolean flags", () => {
expect(parseArgs(["--ubuntu-runner", "", "--mode", "both"])).toEqual({
"ubuntu-runner": "",
mode: "both",
});
});
it("detects release refs and keeps branch refs out of release-only logic", () => {
expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true);
expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true);
expect(looksLikeReleaseVersionRef("v2026.4.5-beta.1")).toBe(true);
expect(looksLikeReleaseVersionRef("v2026.4.7-1")).toBe(true);
expect(looksLikeReleaseVersionRef("main")).toBe(false);
expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false);
});
it("normalizes full Git refs before suite and update decisions", () => {
expect(normalizeRequestedRef(" refs/heads/main ")).toBe("main");
expect(normalizeRequestedRef("refs/tags/v2026.4.14")).toBe("v2026.4.14");
expect(isImmutableReleaseRef("refs/tags/test-tag")).toBe(true);
expect(resolveRequestedSuites("both", "refs/tags/v2026.4.14")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
expect(resolveRequestedSuites("both", "refs/tags/test-tag")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
expect(shouldRunMainChannelDevUpdate("refs/heads/main")).toBe(true);
expect(shouldRunMainChannelDevUpdate("refs/tags/main")).toBe(false);
});
it("skips the dev-update suite for immutable release refs", () => {
expect(resolveRequestedSuites("both", "v2026.4.5")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("skips dev-update for non-main branch validation refs", () => {
expect(resolveRequestedSuites("both", "codex/cross-os-release-checks")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("keeps dev-update enabled for main validation refs", () => {
expect(resolveRequestedSuites("both", "main")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
"dev-update",
]);
});
it("skips dev-update for pinned commit refs", () => {
expect(resolveRequestedSuites("both", "08753a1d793c040b101c8a26c43445dbbab14995")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("builds a suite-aware runner matrix with the beefy Windows default", () => {
const matrix = resolveRunnerMatrix({
mode: "both",
ref: "main",
ubuntuRunner: "",
windowsRunner: "",
macosRunner: "",
varUbuntuRunner: "",
varWindowsRunner: "",
varMacosRunner: "",
});
expect(matrix.include).toHaveLength(12);
expect(matrix.include).toContainEqual(
expect.objectContaining({
os_id: "windows",
runner: "blacksmith-32vcpu-windows-2025",
suite: "dev-update",
lane: "upgrade",
}),
);
expect(matrix.include).toContainEqual(
expect.objectContaining({
os_id: "ubuntu",
suite: "installer-fresh",
lane: "fresh",
}),
);
});
it("can rebuild the Windows PATH with or without current-process entries", () => {
expect(buildWindowsPathBootstrapScript()).toContain("@($userPath, $machinePath, $env:Path)");
const persistedOnlyScript = buildWindowsPathBootstrapScript({
includeCurrentProcessPath: false,
});
expect(persistedOnlyScript).toContain("@($userPath, $machinePath)");
expect(persistedOnlyScript).not.toContain("@($userPath, $machinePath, $env:Path)");
});
it("prefers the freshly installed Windows CLI under npm's prefix before PATH lookup", () => {
const script = buildWindowsFreshShellVersionCheckScript({
expectedNeedle: "2026.4.14",
});
expect(script).toContain(buildWindowsPathBootstrapScript());
expect(script).not.toContain(
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
);
expect(script).toContain("Get-Command npm.cmd -ErrorAction SilentlyContinue");
expect(script).toContain('$env:Path = "$npmPrefix;$env:Path"');
expect(script).toContain("(Join-Path $npmPrefix 'openclaw.cmd')");
expect(script).toContain("$cmd = Get-Command openclaw -ErrorAction Stop");
});
it("keeps Windows dev-update toolchain checks compatible with setup-node PATH shims", () => {
const script = buildWindowsDevUpdateToolchainCheckScript();
expect(script).toContain(buildWindowsPathBootstrapScript());
expect(script).not.toContain(
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
);
expect(script).toContain("$pnpmPath = Resolve-CommandPath 'pnpm'");
expect(script).toContain("$corepackPath = Resolve-CommandPath 'corepack'");
expect(script).toContain("$npmPath = Resolve-CommandPath 'npm'");
});
it("prefers workflow-injected runner override env names over legacy ones", () => {
expect(
readRunnerOverrideEnv({
VAR_UBUNTU_RUNNER: "workflow-linux",
VAR_WINDOWS_RUNNER: "workflow-windows",
VAR_MACOS_RUNNER: "workflow-macos",
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
}),
).toEqual({
varUbuntuRunner: "workflow-linux",
varWindowsRunner: "workflow-windows",
varMacosRunner: "workflow-macos",
});
});
it("falls back to legacy runner override env names when workflow vars are blank", () => {
expect(
readRunnerOverrideEnv({
VAR_UBUNTU_RUNNER: "",
VAR_WINDOWS_RUNNER: " ",
VAR_MACOS_RUNNER: "",
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
}),
).toEqual({
varUbuntuRunner: "legacy-linux",
varWindowsRunner: "legacy-windows",
varMacosRunner: "legacy-macos",
});
});
it("serves installer scripts as UTF-8 text and package payloads as binary", () => {
expect(resolveStaticFileContentType("scripts/install.sh")).toBe("text/plain; charset=utf-8");
expect(resolveStaticFileContentType("scripts/install.ps1")).toBe("text/plain; charset=utf-8");
expect(resolveStaticFileContentType("openclaw-2026.4.14.tgz")).toBe("application/octet-stream");
});
it("uses the published installer URLs for native installer lanes", () => {
expect(resolvePublishedInstallerUrl("darwin")).toBe("https://openclaw.ai/install.sh");
expect(resolvePublishedInstallerUrl("linux")).toBe("https://openclaw.ai/install.sh");
expect(resolvePublishedInstallerUrl("win32")).toBe("https://openclaw.ai/install.ps1");
});
it("uses managed gateway services only on native Windows runners", () => {
expect(shouldUseManagedGatewayService("win32")).toBe(true);
expect(shouldUseManagedGatewayService("darwin")).toBe(false);
expect(shouldUseManagedGatewayService("linux")).toBe(false);
});
it("keeps the Windows installer runtime on the manual gateway after managed lifecycle checks", () => {
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("win32")).toBe(true);
expect(shouldUseManagedGatewayForInstallerRuntime("win32")).toBe(false);
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("darwin")).toBe(false);
expect(shouldUseManagedGatewayForInstallerRuntime("darwin")).toBe(false);
});
it("stops the managed gateway before the manual fallback only on Windows", () => {
expect(shouldStopManagedGatewayBeforeManualFallback("win32")).toBe(true);
expect(shouldStopManagedGatewayBeforeManualFallback("darwin")).toBe(false);
expect(shouldStopManagedGatewayBeforeManualFallback("linux")).toBe(false);
});
it("skips daemon health during installed onboarding only on native Windows", () => {
expect(shouldSkipInstallerDaemonHealthCheck("win32")).toBe(true);
expect(shouldSkipInstallerDaemonHealthCheck("darwin")).toBe(false);
expect(shouldSkipInstallerDaemonHealthCheck("linux")).toBe(false);
});
it("normalizes Windows installed CLI paths to the cmd shim", () => {
expect(
normalizeWindowsInstalledCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
expect(
normalizeWindowsInstalledCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`,
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
});
it("normalizes generic Windows PowerShell shims to cmd shims", () => {
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\pnpm.ps1`)).toBe(
String.raw`C:\Program Files\nodejs\pnpm.cmd`,
);
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\corepack.ps1`)).toBe(
String.raw`C:\Program Files\nodejs\corepack.cmd`,
);
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\node.exe`)).toBe(
String.raw`C:\Program Files\nodejs\node.exe`,
);
});
it("derives the installed prefix from resolved CLI paths", () => {
expect(
resolveInstalledPrefixDirFromCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
"win32",
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm`);
expect(
resolveInstalledPrefixDirFromCliPath("/Users/runner/.npm-global/bin/openclaw", "darwin"),
).toBe("/Users/runner/.npm-global");
});
it("detects whether a managed gateway listener is still reachable on loopback", async () => {
const server = createNetServer();
await new Promise((resolvePromise) => {
server.listen(0, "127.0.0.1", resolvePromise);
});
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
expect(await canConnectToLoopbackPort(port)).toBe(true);
await new Promise((resolvePromise) => {
server.close(resolvePromise);
});
expect(await canConnectToLoopbackPort(port)).toBe(false);
});
it("writes Discord smoke config using the strict guild channel schema", () => {
expect(buildDiscordSmokeGuildsConfig("guild-123", "channel-456")).toEqual({
"guild-123": {
channels: {
"channel-456": {
enabled: true,
requireMention: false,
},
},
},
});
});
it("keeps the dev-update lane for main only", () => {
expect(shouldRunMainChannelDevUpdate("main")).toBe(true);
expect(shouldRunMainChannelDevUpdate("08753a1d793c040b101c8a26c43445dbbab14995")).toBe(false);
expect(shouldRunMainChannelDevUpdate(" codex/cross-os-release-checks-full-native-e2e ")).toBe(
false,
);
expect(shouldRunMainChannelDevUpdate("v2026.4.14")).toBe(false);
});
it("verifies main dev updates against the prepared source sha when available", () => {
expect(resolveDevUpdateVerificationRef("main")).toBe("main");
expect(
resolveDevUpdateVerificationRef("main", "08753a1d793c040b101c8a26c43445dbbab14995"),
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
expect(
resolveDevUpdateVerificationRef(
"refs/heads/main",
"08753a1d793c040b101c8a26c43445dbbab14995",
),
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
expect(resolveDevUpdateVerificationRef("codex/cross-os-release-checks-full-native-e2e")).toBe(
"codex/cross-os-release-checks-full-native-e2e",
);
});
it("drops the bundled plugin postinstall disable flag for real updater calls", () => {
expect(
buildRealUpdateEnv({
FOO: "bar",
OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: "1",
}),
).toEqual({
FOO: "bar",
});
});
it("only treats pinned baseline specs as exact installer version assertions", () => {
expect(resolveExplicitBaselineVersion("")).toBe("");
expect(resolveExplicitBaselineVersion("openclaw@latest")).toBe("");
expect(resolveExplicitBaselineVersion("openclaw@2026.4.10")).toBe("2026.4.10");
expect(resolveExplicitBaselineVersion("2026.4.10")).toBe("2026.4.10");
});
it("reads an installed baseline version without requiring build metadata", () => {
const prefixDir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-installed-version-"));
try {
const packageRoot =
process.platform === "win32"
? join(prefixDir, "node_modules", "openclaw")
: join(prefixDir, "lib", "node_modules", "openclaw");
mkdirSync(packageRoot, { recursive: true });
writeFileSync(
join(packageRoot, "package.json"),
JSON.stringify({
name: "openclaw",
version: "2026.4.10",
}),
"utf8",
);
expect(readInstalledVersion(prefixDir)).toBe("2026.4.10");
} finally {
rmSync(prefixDir, { recursive: true, force: true });
}
});
it("treats missing package scripts as optional in older refs", () => {
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-scripts-"));
try {
writeFileSync(
join(packageRoot, "package.json"),
JSON.stringify({
name: "openclaw",
scripts: {
build: "pnpm build",
},
}),
"utf8",
);
expect(packageHasScript(packageRoot, "build")).toBe(true);
expect(packageHasScript(packageRoot, "ui:build")).toBe(false);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("accepts a git main dev-channel update status payload", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "main",
},
},
channel: {
value: "dev",
},
}),
),
).not.toThrow();
});
it("accepts a git dev-channel payload for a requested non-main branch", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "codex/cross-os-release-checks-full-native-e2e",
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "codex/cross-os-release-checks-full-native-e2e" },
),
).not.toThrow();
});
it("accepts a git dev-channel payload pinned to a prepared source sha", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "main",
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "08753a1d793c040b101c8a26c43445dbbab14995" },
),
).not.toThrow();
});
it("accepts uppercase requested commit shas when update status reports lowercase", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "08753A1D793C040B101C8A26C43445DBBAB14995" },
),
).not.toThrow();
});
it("rejects update status payloads that are not on dev/main git", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "package",
git: {
branch: "release",
},
},
channel: {
value: "stable",
},
}),
),
).toThrow("git install");
});
});

View file

@ -0,0 +1,61 @@
import fs from "node:fs";
import { describe, expect, it } from "vitest";
type OxlintConfig = {
ignorePatterns?: string[];
};
type OxlintTsconfig = {
include?: string[];
exclude?: string[];
};
function readJson(path: string): unknown {
return JSON.parse(fs.readFileSync(path, "utf8")) as unknown;
}
describe("oxlint config", () => {
it("includes bundled extensions in type-aware lint coverage", () => {
const tsconfig = readJson<OxlintTsconfig>("tsconfig.oxlint.json");
expect(tsconfig.include).toContain("extensions/**/*");
expect(tsconfig.exclude ?? []).not.toContain("extensions");
});
it("includes scripts in root type-aware lint coverage", () => {
const tsconfig = readJson<OxlintTsconfig>("tsconfig.oxlint.json");
expect(tsconfig.include).toContain("scripts/**/*");
});
it("has a discoverable scripts tsconfig for type-aware linting", () => {
const tsconfig = readJson<OxlintTsconfig>("scripts/tsconfig.json");
expect(tsconfig.include).toContain("**/*.ts");
expect(tsconfig.exclude ?? []).not.toContain("**/*.ts");
});
it("has a discoverable test tsconfig for type-aware linting", () => {
const tsconfig = readJson<OxlintTsconfig>("test/tsconfig.json");
expect(tsconfig.include).toContain("**/*.ts");
expect(tsconfig.exclude ?? []).not.toContain("**/*.ts");
});
it("does not ignore the bundled extensions tree", () => {
const config = readJson<OxlintConfig>(".oxlintrc.json");
expect(config.ignorePatterns ?? []).not.toContain("extensions/");
});
it("keeps generated and vendored extension outputs ignored", () => {
const config = readJson<OxlintConfig>(".oxlintrc.json");
const ignorePatterns = config.ignorePatterns ?? [];
expect(ignorePatterns).toContain("**/node_modules/**");
expect(ignorePatterns).toContain("**/dist/**");
expect(ignorePatterns).toContain("**/build/**");
expect(ignorePatterns).toContain("**/coverage/**");
expect(ignorePatterns).toContain("**/.cache/**");
});
});

View file

@ -0,0 +1,14 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const SCRIPT_PATH = "scripts/e2e/parallels-npm-update-smoke.sh";
describe("parallels npm update smoke", () => {
it("does not leave guard/server children attached to the wrapper", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain('wait "$SERVER_PID" 2>/dev/null || true');
expect(script).toContain(") >&2 &");
expect(script).toContain('wait "$pid" 2>/dev/null || true');
});
});

View file

@ -0,0 +1,233 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectProdResolvedPackagesFromLockfile,
createBulkAdvisoryPayload,
filterFindingsBySeverity,
parseSnapshotKey,
runPnpmAuditProd,
stripVersionDecorators,
} from "../../scripts/pre-commit/pnpm-audit-prod.mjs";
describe("pnpm-audit-prod", () => {
it("parses scoped snapshot keys with peer suffixes", () => {
expect(parseSnapshotKey("@scope/pkg@1.2.3(peer@4.5.6)")).toEqual({
packageName: "@scope/pkg",
reference: "1.2.3(peer@4.5.6)",
version: "1.2.3",
});
});
it("strips peer and patch decorators from resolved versions", () => {
expect(stripVersionDecorators("7.0.0-rc.9(patch_hash=abc123)(sharp@0.34.5)")).toBe(
"7.0.0-rc.9",
);
expect(stripVersionDecorators("1.2.3")).toBe("1.2.3");
});
it("collects the production graph from pnpm lockfile snapshots", () => {
const lockfile = `lockfileVersion: '9.0'
importers:
.:
dependencies:
pkg-a:
version: 1.0.0
devDependencies:
dev-only:
version: 9.9.9
extensions/demo:
dependencies:
'@scope/pkg':
version: 2.0.0(peer@4.0.0)
workspace-lib:
version: link:../../packages/workspace-lib
snapshots:
pkg-a@1.0.0:
dependencies:
transitive: 3.0.0(patch_hash=abc123)
transitive@3.0.0(patch_hash=abc123): {}
'@scope/pkg@2.0.0(peer@4.0.0)':
optionalDependencies:
opt-dep: 4.0.0
opt-dep@4.0.0: {}
`;
const payload = createBulkAdvisoryPayload(collectProdResolvedPackagesFromLockfile(lockfile));
expect(payload).toEqual({
"@scope/pkg": ["2.0.0"],
"opt-dep": ["4.0.0"],
"pkg-a": ["1.0.0"],
transitive: ["3.0.0"],
});
});
it("resolves npm alias snapshots to the real package name", () => {
const lockfile = `lockfileVersion: '9.0'
importers:
.:
dependencies:
request:
version: npm:@cypress/request@3.0.10
snapshots:
'@cypress/request@3.0.10': {}
`;
const payload = createBulkAdvisoryPayload(collectProdResolvedPackagesFromLockfile(lockfile));
expect(payload).toEqual({
"@cypress/request": ["3.0.10"],
});
});
it("reads inline importer dependency maps without repo dependencies", () => {
const lockfile = `lockfileVersion: '9.0'
importers:
.:
dependencies:
axios: {specifier: ^1.0.0, version: 1.0.0}
'@scope/pkg': {'version': '2.0.0(peer@4.0.0)'}
snapshots:
axios@1.0.0: {}
'@scope/pkg@2.0.0(peer@4.0.0)': {}
`;
const payload = createBulkAdvisoryPayload(collectProdResolvedPackagesFromLockfile(lockfile));
expect(payload).toEqual({
"@scope/pkg": ["2.0.0"],
axios: ["1.0.0"],
});
});
it("resolves quoted snapshot keys that contain tarball URLs", () => {
const lockfile = `lockfileVersion: '9.0'
importers:
.:
dependencies:
wrapper:
version: 1.0.0
snapshots:
wrapper@1.0.0:
dependencies:
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/abc123'
'@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/abc123':
dependencies:
curve25519-js: 0.0.4
curve25519-js@0.0.4: {}
`;
const payload = createBulkAdvisoryPayload(collectProdResolvedPackagesFromLockfile(lockfile));
expect(payload).toEqual({
"@whiskeysockets/libsignal-node": [
"https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/abc123",
],
"curve25519-js": ["0.0.4"],
wrapper: ["1.0.0"],
});
});
it("filters advisory findings by minimum severity", () => {
const findings = filterFindingsBySeverity(
{
axios: [
{
id: "GHSA-low",
severity: "moderate",
title: "moderate issue",
},
{
id: "GHSA-high",
severity: "high",
title: "high issue",
url: "https://github.com/advisories/GHSA-high",
},
],
},
"high",
);
expect(findings).toEqual([
{
id: "GHSA-high",
packageName: "axios",
severity: "high",
title: "high issue",
url: "https://github.com/advisories/GHSA-high",
vulnerableVersions: null,
},
]);
});
it("returns a failing exit code when bulk advisories include high severity findings", async () => {
const tempDir = await mkdtemp(path.join(tmpdir(), "openclaw-audit-prod-"));
await writeFile(
path.join(tempDir, "pnpm-lock.yaml"),
`lockfileVersion: '9.0'
importers:
.:
dependencies:
axios:
version: 1.0.0
snapshots:
axios@1.0.0: {}
`,
"utf8",
);
try {
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
const exitCode = await runPnpmAuditProd({
rootDir: tempDir,
fetchImpl: async () =>
new Response(
JSON.stringify({
axios: [
{
id: "GHSA-test",
severity: "high",
title: "test issue",
vulnerable_versions: "<=1.0.0",
url: "https://github.com/advisories/GHSA-test",
},
],
}),
{
status: 200,
headers: {
"content-type": "application/json",
},
},
),
stdout: {
write(chunk: string) {
stdoutChunks.push(chunk);
return true;
},
} as NodeJS.WriteStream,
stderr: {
write(chunk: string) {
stderrChunks.push(chunk);
return true;
},
} as NodeJS.WriteStream,
});
expect(exitCode).toBe(1);
expect(stdoutChunks).toEqual([]);
expect(stderrChunks.join("")).toContain("Found 1 high or higher advisories");
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});
});

View file

@ -0,0 +1,165 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createPnpmRunnerSpawnSpec, resolvePnpmRunner } from "../../scripts/pnpm-runner.mjs";
describe("resolvePnpmRunner", () => {
it("uses npm_execpath when it points to a JS pnpm entrypoint", () => {
expect(
resolvePnpmRunner({
npmExecPath: "/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: [
"/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
"exec",
"vitest",
"run",
],
shell: false,
});
});
it("uses npm_execpath when it points to a shebang pnpm script", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-"));
const npmExecPath = path.join(tempDir, "pnpm");
writeFileSync(npmExecPath, "#!/usr/bin/env node\nconsole.log('pnpm');\n");
try {
expect(
resolvePnpmRunner({
npmExecPath,
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: [npmExecPath, "exec", "vitest", "run"],
shell: false,
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("prepends node args when launching pnpm through node", () => {
expect(
resolvePnpmRunner({
npmExecPath: "/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
nodeArgs: ["--no-maglev"],
nodeExecPath: "/usr/local/bin/node",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "/usr/local/bin/node",
args: [
"--no-maglev",
"/home/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
"exec",
"vitest",
"run",
],
shell: false,
});
});
it("falls back to bare pnpm when npm_execpath points to a native pnpm binary", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "pnpm-runner-"));
const npmExecPath = path.join(tempDir, "pnpm");
writeFileSync(npmExecPath, Buffer.from([0x7f, 0x45, 0x4c, 0x46]));
try {
expect(
resolvePnpmRunner({
npmExecPath,
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("falls back to bare pnpm on non-Windows when npm_execpath is missing", () => {
expect(
resolvePnpmRunner({
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
shell: false,
});
});
it("wraps pnpm.cmd via cmd.exe on Windows when npm_execpath is unavailable", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run", "-t", "path with spaces"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", 'pnpm.cmd exec vitest run -t "path with spaces"'],
shell: false,
windowsVerbatimArguments: true,
});
});
it("escapes caret arguments for Windows cmd.exe", () => {
expect(
resolvePnpmRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "-t", "@scope/pkg@^1.2.3"],
platform: "win32",
}),
).toEqual({
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "pnpm.cmd exec vitest -t @scope/pkg@^^1.2.3"],
shell: false,
windowsVerbatimArguments: true,
});
});
it("builds a shared spawn spec with inherited stdio and env overrides", () => {
const env = { PATH: "/custom/bin", FOO: "bar" };
expect(
createPnpmRunnerSpawnSpec({
cwd: "/repo",
detached: true,
npmExecPath: "",
pnpmArgs: ["exec", "vitest", "run"],
platform: "linux",
env,
}),
).toEqual({
command: "pnpm",
args: ["exec", "vitest", "run"],
options: {
cwd: "/repo",
detached: true,
stdio: "inherit",
env,
shell: false,
windowsVerbatimArguments: undefined,
},
});
});
});

View file

@ -0,0 +1,736 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
createNestedNpmInstallEnv,
pruneInstalledPackageDist,
discoverBundledPluginRuntimeDeps,
pruneBundledPluginSourceNodeModules,
restoreLegacyUpdaterCompatSidecars,
runBundledPluginPostinstall,
} from "../../scripts/postinstall-bundled-plugins.mjs";
import { writePackageDistInventory } from "../../src/infra/package-dist-inventory.ts";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDirAsync } = createScriptTestHarness();
async function createExtensionsDir() {
const root = await createTempDirAsync("openclaw-postinstall-");
const extensionsDir = path.join(root, "dist", "extensions");
await fs.mkdir(extensionsDir, { recursive: true });
return extensionsDir;
}
async function writePluginPackage(
extensionsDir: string,
pluginId: string,
packageJson: Record<string, unknown>,
) {
const pluginDir = path.join(extensionsDir, pluginId);
await fs.mkdir(pluginDir, { recursive: true });
await fs.writeFile(
path.join(pluginDir, "package.json"),
`${JSON.stringify(packageJson, null, 2)}\n`,
);
const packageRoot =
path.basename(path.dirname(extensionsDir)) === "dist"
? path.dirname(path.dirname(extensionsDir))
: path.dirname(extensionsDir);
try {
await writePackageDistInventory(packageRoot);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
}
}
describe("bundled plugin postinstall", () => {
function createNpmInstallArgs(...packages: string[]) {
return [
"install",
"--omit=dev",
"--no-save",
"--package-lock=false",
"--legacy-peer-deps",
...packages,
];
}
function createBareNpmRunner(packages: string[]) {
return {
command: "npm",
args: createNpmInstallArgs(...packages),
env: {
HOME: "/tmp/home",
PATH: "/tmp/node/bin",
},
shell: false as const,
};
}
function expectNpmInstallSpawn(
spawnSync: ReturnType<typeof vi.fn>,
packageRoot: string,
packages: string[],
) {
expect(spawnSync).toHaveBeenCalledWith("npm", createNpmInstallArgs(...packages), {
cwd: packageRoot,
encoding: "utf8",
env: {
HOME: "/tmp/home",
PATH: "/tmp/node/bin",
},
shell: false,
stdio: "pipe",
windowsVerbatimArguments: undefined,
});
}
it("clears global npm config before nested installs", () => {
expect(
createNestedNpmInstallEnv({
npm_config_global: "true",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
}),
).toEqual({
HOME: "/tmp/home",
});
});
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));
await writePluginPackage(extensionsDir, "acpx", {
dependencies: {
acpx: "0.4.1",
},
});
const spawnSync = vi.fn();
runBundledPluginPostinstall({
env: { HOME: "/tmp/home" },
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["acpx@0.4.1"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expect(spawnSync).not.toHaveBeenCalled();
});
it("prunes source-checkout bundled plugin node_modules", async () => {
const packageRoot = await createTempDirAsync("openclaw-source-checkout-");
const extensionsDir = path.join(packageRoot, "extensions");
await fs.mkdir(path.join(packageRoot, ".git"), { recursive: true });
await fs.mkdir(path.join(packageRoot, "src"), { recursive: true });
await fs.mkdir(extensionsDir, { recursive: true });
await writePluginPackage(extensionsDir, "acpx", {
dependencies: {
acpx: "0.5.2",
},
});
await fs.mkdir(path.join(extensionsDir, "acpx", "node_modules", "acpx"), { recursive: true });
await fs.writeFile(
path.join(extensionsDir, "acpx", "node_modules", "acpx", "package.json"),
JSON.stringify({ name: "acpx", version: "0.4.1" }),
);
const spawnSync = vi.fn();
runBundledPluginPostinstall({
env: { HOME: "/tmp/home" },
packageRoot,
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
await expect(fs.stat(path.join(extensionsDir, "acpx", "node_modules"))).rejects.toMatchObject({
code: "ENOENT",
});
expect(spawnSync).not.toHaveBeenCalled();
});
it("keeps source-checkout prune non-fatal", async () => {
const packageRoot = await createTempDirAsync("openclaw-source-checkout-prune-error-");
const extensionsDir = path.join(packageRoot, "extensions");
await fs.mkdir(path.join(packageRoot, ".git"), { recursive: true });
await fs.mkdir(path.join(packageRoot, "src"), { recursive: true });
await fs.mkdir(path.join(extensionsDir, "acpx"), { recursive: true });
await fs.writeFile(path.join(extensionsDir, "acpx", "package.json"), "{}\n");
const warn = vi.fn();
expect(() =>
runBundledPluginPostinstall({
env: { HOME: "/tmp/home" },
packageRoot,
rmSync: vi.fn(() => {
throw new Error("locked");
}),
log: { log: vi.fn(), warn },
}),
).not.toThrow();
expect(warn).toHaveBeenCalledWith(
"[postinstall] could not prune bundled plugin source node_modules: Error: locked",
);
});
it("honors disable env before source-checkout pruning", async () => {
const packageRoot = await createTempDirAsync("openclaw-source-checkout-disabled-");
const extensionsDir = path.join(packageRoot, "extensions");
await fs.mkdir(path.join(packageRoot, ".git"), { recursive: true });
await fs.mkdir(path.join(packageRoot, "src"), { recursive: true });
await fs.mkdir(path.join(extensionsDir, "acpx", "node_modules"), { recursive: true });
await fs.writeFile(path.join(extensionsDir, "acpx", "package.json"), "{}\n");
runBundledPluginPostinstall({
env: { OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: "1" },
packageRoot,
log: { log: vi.fn(), warn: vi.fn() },
});
await expect(fs.stat(path.join(extensionsDir, "acpx", "node_modules"))).resolves.toBeTruthy();
});
it("prunes stale dist files from packaged installs", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-");
const currentFile = path.join(packageRoot, "dist", "channel-BOa4MfoC.js");
const staleFile = path.join(packageRoot, "dist", "channel-CJUAgRQR.js");
await fs.mkdir(path.dirname(currentFile), { recursive: true });
await fs.writeFile(currentFile, "export {};\n");
await writePackageDistInventory(packageRoot);
await fs.writeFile(staleFile, "export {};\n");
expect(
pruneInstalledPackageDist({
packageRoot,
log: { log: vi.fn(), warn: vi.fn() },
}),
).toEqual(["dist/channel-CJUAgRQR.js"]);
await expect(fs.stat(currentFile)).resolves.toBeTruthy();
await expect(fs.stat(staleFile)).rejects.toMatchObject({ code: "ENOENT" });
});
it("restores only postinstall-generated QA compat sidecars after pruning old installs", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-qa-compat-");
const currentFile = path.join(packageRoot, "dist", "entry.js");
const stalePackage = path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json");
const staleManifest = path.join(
packageRoot,
"dist",
"extensions",
"qa-lab",
"openclaw.plugin.json",
);
await fs.mkdir(path.dirname(stalePackage), { recursive: true });
await fs.writeFile(currentFile, "export {};\n");
await writePackageDistInventory(packageRoot);
await fs.writeFile(stalePackage, "{}\n");
await fs.writeFile(staleManifest, "{}\n");
runBundledPluginPostinstall({
packageRoot,
spawnSync: vi.fn(),
log: { log: vi.fn(), warn: vi.fn() },
});
await expect(fs.stat(stalePackage)).rejects.toMatchObject({ code: "ENOENT" });
await expect(fs.stat(staleManifest)).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.readFile(
path.join(packageRoot, "dist", "extensions", "qa-channel", "runtime-api.js"),
"utf8",
),
).resolves.toContain("QA channel implementation is not packaged");
await expect(
fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"),
).resolves.toContain("QA lab implementation is not packaged");
});
it("creates only empty QA compat sidecars for fresh installs", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-no-qa-compat-");
await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
await fs.writeFile(path.join(packageRoot, "dist", "entry.js"), "export {};\n");
await writePackageDistInventory(packageRoot);
expect(
restoreLegacyUpdaterCompatSidecars({
packageRoot,
removedFiles: ["dist/entry-old.js"],
log: { log: vi.fn(), warn: vi.fn() },
}),
).toEqual([
"dist/extensions/qa-channel/runtime-api.js",
"dist/extensions/qa-lab/runtime-api.js",
]);
await expect(
fs.readFile(
path.join(packageRoot, "dist", "extensions", "qa-channel", "runtime-api.js"),
"utf8",
),
).resolves.toBe(
"// Compatibility stub for older OpenClaw updaters. The QA channel implementation is not packaged.\nexport {};\n",
);
await expect(
fs.readFile(path.join(packageRoot, "dist", "extensions", "qa-lab", "runtime-api.js"), "utf8"),
).resolves.toBe(
"// Compatibility stub for older OpenClaw updaters. The QA lab implementation is not packaged.\nexport {};\n",
);
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-channel", "package.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-channel", "openclaw.plugin.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "package.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.stat(path.join(packageRoot, "dist", "extensions", "qa-lab", "openclaw.plugin.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
it("keeps packaged postinstall non-fatal when the dist inventory is missing", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-missing-inventory-");
const staleFile = path.join(packageRoot, "dist", "channel-CJUAgRQR.js");
await fs.mkdir(path.dirname(staleFile), { recursive: true });
await fs.writeFile(staleFile, "export {};\n");
const warn = vi.fn();
expect(() =>
runBundledPluginPostinstall({
packageRoot,
log: { log: vi.fn(), warn },
}),
).not.toThrow();
await expect(fs.stat(staleFile)).resolves.toBeTruthy();
expect(warn).toHaveBeenCalledWith(
"[postinstall] skipping dist prune: missing dist inventory: dist/postinstall-inventory.json",
);
});
it("keeps packaged postinstall non-fatal when the dist inventory is invalid", async () => {
const packageRoot = await createTempDirAsync("openclaw-packaged-install-invalid-inventory-");
const currentFile = path.join(packageRoot, "dist", "channel-BOa4MfoC.js");
const inventoryPath = path.join(packageRoot, "dist", "postinstall-inventory.json");
await fs.mkdir(path.dirname(currentFile), { recursive: true });
await fs.writeFile(currentFile, "export {};\n");
await fs.writeFile(inventoryPath, "{not-json}\n");
const warn = vi.fn();
expect(() =>
runBundledPluginPostinstall({
packageRoot,
log: { log: vi.fn(), warn },
}),
).not.toThrow();
await expect(fs.stat(currentFile)).resolves.toBeTruthy();
expect(warn).toHaveBeenCalledWith(
"[postinstall] skipping dist prune: invalid dist inventory: dist/postinstall-inventory.json",
);
});
it("rejects symlinked dist roots in packaged installs", () => {
expect(() =>
pruneInstalledPackageDist({
packageRoot: "/pkg",
expectedFiles: new Set(),
existsSync: vi.fn(() => true),
lstatSync: vi.fn((filePath) => ({
isDirectory: () => filePath === "/pkg/dist",
isSymbolicLink: () => filePath === "/pkg/dist",
})),
realpathSync: vi.fn((filePath) => filePath),
readdirSync: vi.fn(),
rmSync: vi.fn(),
log: { log: vi.fn(), warn: vi.fn() },
}),
).toThrow("unsafe dist root: dist must be a real directory");
});
it("rejects symlink entries in packaged dist trees", () => {
expect(() =>
pruneInstalledPackageDist({
packageRoot: "/pkg",
expectedFiles: new Set(),
existsSync: vi.fn(() => true),
lstatSync: vi.fn(() => ({
isDirectory: () => true,
isSymbolicLink: () => false,
})),
realpathSync: vi.fn((filePath) => filePath),
readdirSync: vi.fn((filePath) => {
if (filePath === "/pkg/dist") {
return [
{
name: "escape",
isDirectory: () => false,
isFile: () => false,
isSymbolicLink: () => true,
},
];
}
return [];
}),
rmSync: vi.fn(),
log: { log: vi.fn(), warn: vi.fn() },
}),
).toThrow("unsafe dist entry: dist/escape");
});
it("unlinks stale files instead of recursive pruning them", () => {
const unlinkSync = vi.fn();
expect(
pruneInstalledPackageDist({
packageRoot: "/pkg",
expectedFiles: new Set(),
existsSync: vi.fn(() => true),
lstatSync: vi.fn(() => ({
isDirectory: () => true,
isSymbolicLink: () => false,
})),
realpathSync: vi.fn((filePath) => filePath),
readdirSync: vi.fn((filePath, options) => {
if (filePath === "/pkg/dist" && options?.withFileTypes) {
return [
{
name: "stale.js",
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
},
];
}
return [];
}),
unlinkSync,
log: { log: vi.fn(), warn: vi.fn() },
}),
).toEqual(["dist/stale.js"]);
expect(unlinkSync).toHaveBeenCalledWith("/pkg/dist/stale.js");
});
it("runs nested local installs with sanitized env when the sentinel package is missing", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "acpx", {
dependencies: {
acpx: "0.4.1",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_global: "true",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["acpx@0.4.1"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["acpx@0.4.1"]);
});
it("skips reinstall when the bundled sentinel package already exists", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "acpx", {
dependencies: {
acpx: "0.4.1",
},
});
await fs.mkdir(path.join(packageRoot, "node_modules", "acpx"), { recursive: true });
await fs.writeFile(
path.join(packageRoot, "node_modules", "acpx", "package.json"),
"{}\n",
"utf8",
);
const spawnSync = vi.fn();
runBundledPluginPostinstall({
env: { npm_config_global: "true" },
extensionsDir,
packageRoot,
spawnSync,
});
expect(spawnSync).not.toHaveBeenCalled();
});
it("reinstalls bundled runtime deps when optional native children are missing", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
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",
},
}),
);
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: { HOME: "/tmp/home", OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1" },
extensionsDir,
packageRoot,
arch: "arm64",
npmRunner: createBareNpmRunner(["@snazzah/davey@0.1.11"]),
platform: "win32",
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["@snazzah/davey@0.1.11"]);
});
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 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",
},
}),
);
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("installs missing bundled plugin runtime deps during global installs", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"@slack/web-api": "7.11.0",
},
});
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_global: "true",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["@slack/web-api@7.11.0", "grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["@slack/web-api@7.11.0", "grammy@1.38.4"]);
});
it("installs only missing bundled plugin runtime deps", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "slack", {
dependencies: {
"@slack/web-api": "7.11.0",
},
});
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
await fs.mkdir(path.join(packageRoot, "node_modules", "@slack", "web-api"), {
recursive: true,
});
await fs.writeFile(
path.join(packageRoot, "node_modules", "@slack", "web-api", "package.json"),
"{}\n",
);
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["grammy@1.38.4"]);
});
it("installs bundled plugin deps when npm location is global", async () => {
const extensionsDir = await createExtensionsDir();
const packageRoot = path.dirname(path.dirname(extensionsDir));
await writePluginPackage(extensionsDir, "telegram", {
dependencies: {
grammy: "1.38.4",
},
});
const spawnSync = vi.fn(() => ({ status: 0, stderr: "", stdout: "" }));
runBundledPluginPostinstall({
env: {
OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS: "1",
npm_config_location: "global",
npm_config_prefix: "/opt/homebrew",
HOME: "/tmp/home",
},
extensionsDir,
packageRoot,
npmRunner: createBareNpmRunner(["grammy@1.38.4"]),
spawnSync,
log: { log: vi.fn(), warn: vi.fn() },
});
expectNpmInstallSpawn(spawnSync, packageRoot, ["grammy@1.38.4"]);
});
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");
await fs.mkdir(path.join(extensionsDir, "acpx", "node_modules"), { recursive: true });
await fs.mkdir(path.join(extensionsDir, "fixtures", "node_modules"), { recursive: true });
await fs.writeFile(
path.join(extensionsDir, "acpx", "package.json"),
JSON.stringify({ name: "@openclaw/acpx" }),
);
pruneBundledPluginSourceNodeModules({ extensionsDir });
await expect(fs.stat(path.join(extensionsDir, "acpx", "node_modules"))).rejects.toMatchObject({
code: "ENOENT",
});
await expect(
fs.stat(path.join(extensionsDir, "fixtures", "node_modules")),
).resolves.toBeTruthy();
});
it("skips symlink entries when pruning source-checkout bundled plugin node_modules", () => {
const removePath = vi.fn();
pruneBundledPluginSourceNodeModules({
extensionsDir: "/repo/extensions",
existsSync: vi.fn((value) => value === "/repo/extensions"),
readdirSync: vi.fn(() => [
{
name: "acpx",
isDirectory: () => true,
isSymbolicLink: () => true,
},
]),
rmSync: removePath,
});
expect(removePath).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,72 @@
import { describe, expect, it, vi } from "vitest";
import {
createPackageManagerWarningMessage,
detectLifecyclePackageManager,
warnIfNonPnpmLifecycle,
} from "../../scripts/preinstall-package-manager-warning.mjs";
describe("detectLifecyclePackageManager", () => {
it("prefers npm_config_user_agent when present", () => {
expect(
detectLifecyclePackageManager({
npm_config_user_agent: "npm/11.4.1 node/v22.20.0 darwin arm64",
}),
).toBe("npm");
});
it("falls back to npm_execpath when user agent is missing", () => {
expect(
detectLifecyclePackageManager({
npm_execpath: "/Users/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
}),
).toBe("pnpm");
});
it("ignores untrusted user-agent tokens with control characters", () => {
expect(
detectLifecyclePackageManager({
npm_config_user_agent: "\u001bnpm/11.4.1 node/v22.20.0 darwin arm64",
npm_execpath: "/Users/test/.cache/node/corepack/v1/pnpm/10.32.1/bin/pnpm.cjs",
}),
).toBe("pnpm");
});
});
describe("createPackageManagerWarningMessage", () => {
it("returns null for pnpm", () => {
expect(createPackageManagerWarningMessage("pnpm")).toBeNull();
});
it("warns for npm installs", () => {
expect(createPackageManagerWarningMessage("npm")).toContain("prefer: corepack pnpm install");
});
});
describe("warnIfNonPnpmLifecycle", () => {
it("warns once for npm lifecycle runs", () => {
const warn = vi.fn();
expect(
warnIfNonPnpmLifecycle(
{
npm_config_user_agent: "npm/11.4.1 node/v22.20.0 darwin arm64",
},
warn,
),
).toBe(true);
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0]?.[0]).toContain("detected npm");
});
it("stays quiet for pnpm", () => {
const warn = vi.fn();
expect(
warnIfNonPnpmLifecycle(
{
npm_config_user_agent: "pnpm/10.32.1 npm/? node/v22.20.0 darwin arm64",
},
warn,
),
).toBe(false);
expect(warn).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,97 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
createPrefixedOutputWriter,
isArtifactSetFresh,
parseMode,
runNodeStepsInParallel,
} from "../../scripts/prepare-extension-package-boundary-artifacts.mjs";
const tempRoots = new Set<string>();
afterEach(() => {
for (const rootDir of tempRoots) {
fs.rmSync(rootDir, { force: true, recursive: true });
}
tempRoots.clear();
});
describe("prepare-extension-package-boundary-artifacts", () => {
it("prefixes each completed line and flushes the trailing partial line", () => {
let output = "";
const writer = createPrefixedOutputWriter("boundary", {
write(chunk: string) {
output += chunk;
},
});
writer.write("first line\nsecond");
writer.write(" line\nthird");
writer.flush();
expect(output).toBe("[boundary] first line\n[boundary] second line\n[boundary] third");
});
it("aborts sibling steps after the first failure", async () => {
const startedAt = Date.now();
const slowStepTimeoutMs = 60_000;
const abortBudgetMs = 30_000;
await expect(
runNodeStepsInParallel([
{
label: "fail-fast",
args: ["--eval", "process.exit(2)"],
timeoutMs: slowStepTimeoutMs,
},
{
label: "slow-step",
args: ["--eval", "setTimeout(() => {}, 60_000)"],
timeoutMs: slowStepTimeoutMs,
},
]),
).rejects.toThrow("fail-fast failed with exit code 2");
expect(Date.now() - startedAt).toBeLessThan(abortBudgetMs);
}, 45_000);
it("treats artifacts as fresh only when outputs are newer than inputs", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-boundary-prep-"));
tempRoots.add(rootDir);
const inputPath = path.join(rootDir, "src", "demo.ts");
const outputPath = path.join(rootDir, "dist", "demo.tsbuildinfo");
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(inputPath, "export const demo = 1;\n", "utf8");
fs.writeFileSync(outputPath, "ok\n", "utf8");
fs.utimesSync(inputPath, new Date(1_000), new Date(1_000));
fs.utimesSync(outputPath, new Date(2_000), new Date(2_000));
expect(
isArtifactSetFresh({
rootDir,
inputPaths: ["src"],
outputPaths: ["dist/demo.tsbuildinfo"],
}),
).toBe(true);
fs.utimesSync(inputPath, new Date(3_000), new Date(3_000));
expect(
isArtifactSetFresh({
rootDir,
inputPaths: ["src"],
outputPaths: ["dist/demo.tsbuildinfo"],
}),
).toBe(false);
});
it("parses prep mode and rejects unknown values", () => {
expect(parseMode([])).toBe("all");
expect(parseMode(["--mode=package-boundary"])).toBe("package-boundary");
expect(() => parseMode(["--mode=nope"])).toThrow("Unknown mode: nope");
});
});

View file

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import {
classifyRootDependencyOwnership,
collectModuleSpecifiers,
} from "../../scripts/root-dependency-ownership-audit.mjs";
describe("collectModuleSpecifiers", () => {
it("captures require.resolve package lookups used by runtime shims and bundled plugins", () => {
expect([
...collectModuleSpecifiers(`
const require = createRequire(import.meta.url);
const runtimeRequire = createRequire(runtimePackagePath);
require.resolve("gaxios");
runtimeRequire.resolve("openshell/package.json");
`),
]).toEqual(["gaxios", "openshell/package.json"]);
});
});
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",
recommendation: "consider moving from dependencies to devDependencies",
});
});
it("treats extension-only deps as localizable when no root mirror exists", () => {
expect(
classifyRootDependencyOwnership({
sections: ["extensions", "test"],
rootMirrorImporters: [],
}),
).toEqual({
category: "extension_only_localizable",
recommendation:
"remove from root package.json and rely on owning extension manifests plus doctor --fix",
});
});
it("treats src-owned deps as core runtime", () => {
expect(
classifyRootDependencyOwnership({
sections: ["src"],
rootMirrorImporters: [],
}),
).toEqual({
category: "core_runtime",
recommendation: "keep at root",
});
});
it("treats unreferenced deps as removal candidates", () => {
expect(
classifyRootDependencyOwnership({
sections: [],
rootMirrorImporters: [],
}),
).toEqual({
category: "unreferenced",
recommendation: "investigate removal; no direct source imports found in scanned files",
});
});
});

View file

@ -0,0 +1,33 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { shouldPrepareExtensionPackageBoundaryArtifacts } from "../../scripts/run-oxlint.mjs";
describe("run-oxlint", () => {
it("prepares extension package boundary artifacts for normal lint runs", () => {
expect(shouldPrepareExtensionPackageBoundaryArtifacts([])).toBe(true);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["src/index.ts"])).toBe(true);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--type-aware"])).toBe(true);
});
it("skips artifact preparation for metadata-only oxlint commands", () => {
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--help"])).toBe(false);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--version"])).toBe(false);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--print-config"])).toBe(false);
expect(shouldPrepareExtensionPackageBoundaryArtifacts(["--rules"])).toBe(false);
});
it("does not run package-boundary artifact prep twice in pnpm check", () => {
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;
};
const shardedLintRunner = readFileSync("scripts/run-oxlint-shards.mjs", "utf8");
expect(packageJson.scripts.check).toBe("node scripts/check.mjs");
expect(packageJson.scripts.lint).toBe("node scripts/run-oxlint-shards.mjs");
expect(packageJson.scripts.check).not.toContain(
"node scripts/prepare-extension-package-boundary-artifacts.mjs",
);
expect(shardedLintRunner).toContain("prepare-extension-package-boundary-artifacts.mjs");
expect(shardedLintRunner).toContain('OPENCLAW_OXLINT_SKIP_PREPARE: "1"');
});
});

View file

@ -0,0 +1,67 @@
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
buildVitestProfileCommand,
parseArgs,
resolveVitestProfileDir,
} from "../../scripts/run-vitest-profile.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
describe("scripts/run-vitest-profile", () => {
const { trackTempDir } = createScriptTestHarness();
it("defaults profile output outside the repo", () => {
const outputDir = trackTempDir(resolveVitestProfileDir({ mode: "main", outputDir: "" }));
expect(outputDir.startsWith(os.tmpdir())).toBe(true);
expect(outputDir.startsWith(process.cwd())).toBe(false);
});
it("keeps explicit output directories", () => {
expect(
resolveVitestProfileDir({ mode: "runner", outputDir: ".artifacts/custom-profile" }),
).toBe(path.resolve(".artifacts/custom-profile"));
});
it("builds main-thread cpu profiling args", () => {
expect(buildVitestProfileCommand({ mode: "main", outputDir: "/tmp/profile-main" })).toEqual({
command: process.execPath,
args: [
"--cpu-prof",
"--cpu-prof-dir=/tmp/profile-main",
"./node_modules/vitest/vitest.mjs",
"run",
"--config",
"test/vitest/vitest.unit.config.ts",
"--no-file-parallelism",
],
});
});
it("builds runner cpu and heap profiling args", () => {
expect(buildVitestProfileCommand({ mode: "runner", outputDir: "/tmp/profile-runner" })).toEqual(
{
command: "pnpm",
args: [
"vitest",
"run",
"--config",
"test/vitest/vitest.unit.config.ts",
"--no-file-parallelism",
"--execArgv=--cpu-prof",
"--execArgv=--cpu-prof-dir=/tmp/profile-runner",
"--execArgv=--heap-prof",
"--execArgv=--heap-prof-dir=/tmp/profile-runner",
],
},
);
});
it("parses mode and explicit output dir", () => {
expect(parseArgs(["runner", "--output-dir", "/tmp/out"])).toEqual({
mode: "runner",
outputDir: "/tmp/out",
});
});
});

View file

@ -0,0 +1,124 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import {
installVitestNoOutputWatchdog,
resolveVitestNodeArgs,
resolveVitestNoOutputTimeoutMs,
resolveVitestSpawnParams,
shouldSuppressVitestStderrLine,
} from "../../scripts/run-vitest.mjs";
describe("scripts/run-vitest", () => {
it("adds --no-maglev to vitest child processes by default", () => {
expect(resolveVitestNodeArgs({ PATH: "/usr/bin" })).toEqual(["--no-maglev"]);
});
it("allows opting back into Maglev explicitly", () => {
expect(
resolveVitestNodeArgs({
OPENCLAW_VITEST_ENABLE_MAGLEV: "1",
PATH: "/usr/bin",
}),
).toEqual([]);
});
it("parses the optional no-output timeout env", () => {
expect(resolveVitestNoOutputTimeoutMs({})).toBeNull();
expect(resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "2500" })).toBe(
2500,
);
expect(
resolveVitestNoOutputTimeoutMs({ OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0" }),
).toBeNull();
});
it("spawns vitest in a detached process group on Unix hosts", () => {
expect(resolveVitestSpawnParams({ PATH: "/usr/bin" }, "darwin")).toEqual({
env: { PATH: "/usr/bin" },
detached: true,
stdio: ["inherit", "pipe", "pipe"],
});
expect(resolveVitestSpawnParams({ PATH: "/usr/bin" }, "win32")).toEqual({
env: { PATH: "/usr/bin" },
detached: false,
stdio: ["inherit", "pipe", "pipe"],
});
});
it("suppresses rolldown plugin timing noise while keeping other stderr intact", () => {
expect(
shouldSuppressVitestStderrLine(
"\u001b[33m[PLUGIN_TIMINGS] Warning:\u001b[0m plugin `foo` was slow\n",
),
).toBe(true);
expect(shouldSuppressVitestStderrLine("real failure output\n")).toBe(false);
});
it("kills silent vitest runs after the configured idle timeout", () => {
vi.useFakeTimers();
try {
const stdout = new EventEmitter();
const timeoutSpy = vi.fn();
const forceKillSpy = vi.fn();
const logSpy = vi.fn();
const teardown = installVitestNoOutputWatchdog({
streams: [stdout],
timeoutMs: 1000,
forceKillAfterMs: 5000,
log: logSpy,
onTimeout: timeoutSpy,
onForceKill: forceKillSpy,
setTimeoutFn: setTimeout,
clearTimeoutFn: clearTimeout,
});
vi.advanceTimersByTime(900);
expect(timeoutSpy).not.toHaveBeenCalled();
stdout.emit("data", "still alive");
vi.advanceTimersByTime(900);
expect(timeoutSpy).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(timeoutSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledWith(
"[vitest] no output for 1000ms; terminating stalled Vitest process group.",
);
vi.advanceTimersByTime(5000);
expect(forceKillSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledWith(
"[vitest] process group still alive after 5000ms; sending SIGKILL.",
);
teardown();
} finally {
vi.useRealTimers();
}
});
it("includes the runner label in watchdog logs when provided", () => {
vi.useFakeTimers();
try {
const stdout = new EventEmitter();
const logSpy = vi.fn();
installVitestNoOutputWatchdog({
streams: [stdout],
timeoutMs: 1000,
forceKillAfterMs: 0,
label: "run --config test/vitest/vitest.secrets.config.ts",
log: logSpy,
onTimeout: () => {},
});
vi.advanceTimersByTime(1000);
expect(logSpy).toHaveBeenCalledWith(
"[vitest] no output for 1000ms; terminating stalled Vitest process group (run --config test/vitest/vitest.secrets.config.ts).",
);
} finally {
vi.useRealTimers();
}
});
});

View file

@ -0,0 +1,82 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
copyStaticExtensionAssets,
listStaticExtensionAssetOutputs,
writeStableRootRuntimeAliases,
} from "../../scripts/runtime-postbuild.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
describe("runtime postbuild static assets", () => {
it("tracks plugin-owned static assets that release packaging must ship", () => {
expect(listStaticExtensionAssetOutputs()).toContain(
"dist/extensions/diffs/assets/viewer-runtime.js",
);
});
it("copies declared static assets into dist", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const src = "extensions/acpx/src/runtime-internals/mcp-proxy.mjs";
const dest = "dist/extensions/acpx/mcp-proxy.mjs";
const sourcePath = path.join(rootDir, src);
const destPath = path.join(rootDir, dest);
await fs.mkdir(path.dirname(sourcePath), { recursive: true });
await fs.writeFile(sourcePath, "proxy-data\n", "utf8");
copyStaticExtensionAssets({
rootDir,
assets: [{ src, dest }],
});
expect(await fs.readFile(destPath, "utf8")).toBe("proxy-data\n");
});
it("warns when a declared static asset is missing", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const warn = vi.fn();
copyStaticExtensionAssets({
rootDir,
assets: [{ src: "missing/file.mjs", dest: "dist/file.mjs" }],
warn,
});
expect(warn).toHaveBeenCalledWith(
"[runtime-postbuild] static asset not found, skipping: missing/file.mjs",
);
});
it("writes stable aliases for hashed root runtime modules", async () => {
const rootDir = createTempDir("openclaw-runtime-postbuild-");
const distDir = path.join(rootDir, "dist");
await fs.mkdir(distDir, { recursive: true });
await fs.writeFile(
path.join(distDir, "runtime-model-auth.runtime-XyZ987.js"),
"export const auth = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "runtime-tts.runtime-AbCd1234.js"),
"export const tts = true;\n",
"utf8",
);
await fs.writeFile(
path.join(distDir, "library-Other123.js"),
"export const x = true;\n",
"utf8",
);
writeStableRootRuntimeAliases({ rootDir });
expect(await fs.readFile(path.join(distDir, "runtime-model-auth.runtime.js"), "utf8")).toBe(
'export * from "./runtime-model-auth.runtime-XyZ987.js";\n',
);
expect(await fs.readFile(path.join(distDir, "runtime-tts.runtime.js"), "utf8")).toBe(
'export * from "./runtime-tts.runtime-AbCd1234.js";\n',
);
await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow();
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,66 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs";
async function withTempDir(run: (dir: string) => Promise<void>) {
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-stage-runtime-"));
try {
await run(dir);
} finally {
await fs.promises.rm(dir, { recursive: true, force: true });
}
}
describe("stageBundledPluginRuntime", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("copies files when Windows rejects runtime overlay symlinks", async () => {
await withTempDir(async (repoRoot) => {
const sourceFile = path.join(
repoRoot,
"dist",
"extensions",
"acpx",
"skills",
"acp-router",
"fixture.txt",
);
await fs.promises.mkdir(path.dirname(sourceFile), { recursive: true });
await fs.promises.writeFile(sourceFile, "skill-body\n", "utf8");
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const symlinkSpy = vi
.spyOn(fs, "symlinkSync")
.mockImplementation((target, targetPath, type) => {
if (
String(targetPath).includes(`${path.sep}dist-runtime${path.sep}`) &&
type !== "junction"
) {
const error = new Error("no symlink privilege");
Object.assign(error, { code: "EPERM" });
throw error;
}
return undefined;
});
stageBundledPluginRuntime({ repoRoot });
const runtimeFile = path.join(
repoRoot,
"dist-runtime",
"extensions",
"acpx",
"skills",
"acp-router",
"fixture.txt",
);
expect(await fs.promises.readFile(runtimeFile, "utf8")).toBe("skill-body\n");
expect(fs.lstatSync(runtimeFile).isSymbolicLink()).toBe(false);
expect(symlinkSpy).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,439 @@
import { execFileSync } from "node:child_process";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
detectChangedExtensionIds,
listAvailableExtensionIds,
listChangedExtensionIds,
} from "../../scripts/lib/changed-extensions.mjs";
import {
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
createExtensionTestShards,
resolveExtensionBatchPlan,
resolveExtensionTestPlan,
} from "../../scripts/lib/extension-test-plan.mjs";
import { bundledPluginFile, bundledPluginRoot } from "../helpers/bundled-plugin-paths.js";
const scriptPath = path.join(process.cwd(), "scripts", "test-extension.mjs");
function runScript(args: string[], cwd = process.cwd()) {
return execFileSync(process.execPath, [scriptPath, ...args], {
cwd,
encoding: "utf8",
});
}
function findExtensionWithoutTests() {
const extensionId = listAvailableExtensionIds().find(
(candidate) => !resolveExtensionTestPlan({ targetArg: candidate, cwd: process.cwd() }).hasTests,
);
expect(extensionId).toBeDefined();
return extensionId ?? "missing-no-test-extension";
}
describe("scripts/test-extension.mjs", () => {
it("resolves channel-root extensions onto the channel vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "slack", cwd: process.cwd() });
expect(plan.extensionId).toBe("slack");
expect(plan.extensionDir).toBe(bundledPluginRoot("slack"));
expect(plan.config).toBe("test/vitest/vitest.extension-channels.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("slack"));
expect(plan.hasTests).toBe(true);
});
it("resolves bluebubbles onto the bluebubbles vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "bluebubbles", cwd: process.cwd() });
expect(plan.extensionId).toBe("bluebubbles");
expect(plan.config).toBe("test/vitest/vitest.extension-bluebubbles.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("bluebubbles"));
expect(plan.hasTests).toBe(true);
});
it("resolves acpx onto the acpx vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "acpx", cwd: process.cwd() });
expect(plan.extensionId).toBe("acpx");
expect(plan.config).toBe("test/vitest/vitest.extension-acpx.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("acpx"));
expect(plan.hasTests).toBe(true);
});
it("resolves diffs onto the diffs vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "diffs", cwd: process.cwd() });
expect(plan.extensionId).toBe("diffs");
expect(plan.config).toBe("test/vitest/vitest.extension-diffs.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("diffs"));
expect(plan.hasTests).toBe(true);
});
it("resolves feishu onto the feishu vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "feishu", cwd: process.cwd() });
expect(plan.extensionId).toBe("feishu");
expect(plan.config).toBe("test/vitest/vitest.extension-feishu.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("feishu"));
expect(plan.hasTests).toBe(true);
});
it("resolves provider extensions onto the provider vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "openai", cwd: process.cwd() });
expect(plan.extensionId).toBe("openai");
expect(plan.config).toBe("test/vitest/vitest.extension-providers.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("openai"));
expect(plan.hasTests).toBe(true);
});
it("resolves matrix onto the matrix vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "matrix", cwd: process.cwd() });
expect(plan.extensionId).toBe("matrix");
expect(plan.config).toBe("test/vitest/vitest.extension-matrix.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("matrix"));
expect(plan.hasTests).toBe(true);
});
it("resolves telegram onto the telegram vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "telegram", cwd: process.cwd() });
expect(plan.extensionId).toBe("telegram");
expect(plan.config).toBe("test/vitest/vitest.extension-telegram.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("telegram"));
expect(plan.hasTests).toBe(true);
});
it("resolves whatsapp onto the whatsapp vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "whatsapp", cwd: process.cwd() });
expect(plan.extensionId).toBe("whatsapp");
expect(plan.config).toBe("test/vitest/vitest.extension-whatsapp.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("whatsapp"));
expect(plan.hasTests).toBe(true);
});
it("resolves voice-call onto the voice-call vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "voice-call", cwd: process.cwd() });
expect(plan.extensionId).toBe("voice-call");
expect(plan.config).toBe("test/vitest/vitest.extension-voice-call.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("voice-call"));
expect(plan.hasTests).toBe(true);
});
it("resolves mattermost onto the mattermost vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "mattermost", cwd: process.cwd() });
expect(plan.extensionId).toBe("mattermost");
expect(plan.config).toBe("test/vitest/vitest.extension-mattermost.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("mattermost"));
expect(plan.hasTests).toBe(true);
});
it("resolves irc onto the irc vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "irc", cwd: process.cwd() });
expect(plan.extensionId).toBe("irc");
expect(plan.config).toBe("test/vitest/vitest.extension-irc.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("irc"));
expect(plan.hasTests).toBe(true);
});
it("resolves zalo onto the zalo vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "zalo", cwd: process.cwd() });
expect(plan.extensionId).toBe("zalo");
expect(plan.config).toBe("test/vitest/vitest.extension-zalo.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("zalo"));
expect(plan.hasTests).toBe(true);
});
it("resolves memory extensions onto the memory vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "memory-core", cwd: process.cwd() });
expect(plan.extensionId).toBe("memory-core");
expect(plan.config).toBe("test/vitest/vitest.extension-memory.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("memory-core"));
expect(plan.hasTests).toBe(true);
});
it("resolves msteams onto the msteams vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "msteams", cwd: process.cwd() });
expect(plan.extensionId).toBe("msteams");
expect(plan.config).toBe("test/vitest/vitest.extension-msteams.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("msteams"));
expect(plan.hasTests).toBe(true);
});
it("keeps non-provider extensions on the shared extensions vitest config", () => {
const plan = resolveExtensionTestPlan({ targetArg: "firecrawl", cwd: process.cwd() });
expect(plan.extensionId).toBe("firecrawl");
expect(plan.config).toBe("test/vitest/vitest.extensions.config.ts");
expect(plan.roots).toContain(bundledPluginRoot("firecrawl"));
expect(plan.hasTests).toBe(true);
});
it("omits src/<extension> when no paired core root exists", () => {
const plan = resolveExtensionTestPlan({ targetArg: "line", cwd: process.cwd() });
expect(plan.roots).toContain(bundledPluginRoot("line"));
expect(plan.roots).not.toContain("src/line");
expect(plan.config).toBe("test/vitest/vitest.extension-channels.config.ts");
expect(plan.hasTests).toBe(true);
});
it("infers the extension from the current working directory", () => {
const cwd = path.join(process.cwd(), "extensions", "slack");
const plan = resolveExtensionTestPlan({ cwd });
expect(plan.extensionId).toBe("slack");
expect(plan.extensionDir).toBe(bundledPluginRoot("slack"));
});
it("maps changed paths back to extension ids", () => {
const extensionIds = detectChangedExtensionIds([
bundledPluginFile("slack", "src/channel.ts"),
"src/line/message.test.ts",
bundledPluginFile("firecrawl", "package.json"),
"src/not-a-plugin/file.ts",
]);
expect(extensionIds).toEqual(["firecrawl", "line", "slack"]);
});
it("lists available extension ids", () => {
const extensionIds = listAvailableExtensionIds();
expect(extensionIds).toContain("slack");
expect(extensionIds).toContain("firecrawl");
expect(extensionIds).toEqual(
[...extensionIds].toSorted((left, right) => left.localeCompare(right)),
);
});
it("can fail safe to all extensions when the base revision is unavailable", () => {
const extensionIds = listChangedExtensionIds({
base: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
unavailableBaseBehavior: "all",
});
expect(extensionIds).toEqual(listAvailableExtensionIds());
});
it("resolves a plan for extensions without tests", () => {
const extensionId = findExtensionWithoutTests();
const plan = resolveExtensionTestPlan({ cwd: process.cwd(), targetArg: extensionId });
expect(plan.extensionId).toBe(extensionId);
expect(plan.hasTests).toBe(false);
expect(plan.testFileCount).toBe(0);
});
it("batches extensions into config-specific vitest invocations", () => {
const batch = resolveExtensionBatchPlan({
cwd: process.cwd(),
extensionIds: [
"slack",
"firecrawl",
"line",
"openai",
"matrix",
"telegram",
"mattermost",
"voice-call",
"whatsapp",
"zalo",
"zalouser",
"memory-core",
"msteams",
"feishu",
"irc",
"bluebubbles",
"acpx",
"diffs",
],
});
expect(batch.extensionIds).toEqual([
"acpx",
"bluebubbles",
"diffs",
"feishu",
"firecrawl",
"irc",
"line",
"matrix",
"mattermost",
"memory-core",
"msteams",
"openai",
"slack",
"telegram",
"voice-call",
"whatsapp",
"zalo",
"zalouser",
]);
expect(batch.planGroups).toEqual([
{
config: "test/vitest/vitest.extension-acpx.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["acpx"],
roots: [bundledPluginRoot("acpx")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-bluebubbles.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["bluebubbles"],
roots: [bundledPluginRoot("bluebubbles")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-channels.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["line", "slack"],
roots: [bundledPluginRoot("slack"), bundledPluginRoot("line")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-diffs.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["diffs"],
roots: [bundledPluginRoot("diffs")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-feishu.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["feishu"],
roots: [bundledPluginRoot("feishu")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-irc.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["irc"],
roots: [bundledPluginRoot("irc")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-matrix.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["matrix"],
roots: [bundledPluginRoot("matrix")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-mattermost.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["mattermost"],
roots: [bundledPluginRoot("mattermost")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-memory.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["memory-core"],
roots: [bundledPluginRoot("memory-core")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-msteams.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["msteams"],
roots: [bundledPluginRoot("msteams")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-providers.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["openai"],
roots: [bundledPluginRoot("openai")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-telegram.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["telegram"],
roots: [bundledPluginRoot("telegram")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-voice-call.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["voice-call"],
roots: [bundledPluginRoot("voice-call")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-whatsapp.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["whatsapp"],
roots: [bundledPluginRoot("whatsapp")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extension-zalo.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["zalo", "zalouser"],
roots: [bundledPluginRoot("zalo"), bundledPluginRoot("zalouser")],
testFileCount: expect.any(Number),
},
{
config: "test/vitest/vitest.extensions.config.ts",
estimatedCost: expect.any(Number),
extensionIds: ["firecrawl"],
roots: [bundledPluginRoot("firecrawl")],
testFileCount: expect.any(Number),
},
]);
});
it("balances extension test shards by estimated CI cost", () => {
const shards = createExtensionTestShards({
cwd: process.cwd(),
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
});
expect(shards).toHaveLength(DEFAULT_EXTENSION_TEST_SHARD_COUNT);
expect(shards.map((shard) => shard.checkName)).toEqual(
shards.map((shard, index) => `checks-node-extensions-shard-${index + 1}`),
);
const assigned = shards.flatMap((shard) => shard.extensionIds);
const uniqueAssigned = [...new Set(assigned)];
const expected = listAvailableExtensionIds().filter(
(extensionId) =>
resolveExtensionTestPlan({ cwd: process.cwd(), targetArg: extensionId }).hasTests,
);
expect(uniqueAssigned.toSorted((left, right) => left.localeCompare(right))).toEqual(
expected.toSorted((left, right) => left.localeCompare(right)),
);
expect(assigned).toHaveLength(expected.length);
const totals = shards.map((shard) => shard.estimatedCost);
expect(Math.max(...totals) - Math.min(...totals)).toBeLessThanOrEqual(1);
const msTeamsShardIndex = shards.findIndex((shard) => shard.extensionIds.includes("msteams"));
const feishuShardIndex = shards.findIndex((shard) => shard.extensionIds.includes("feishu"));
expect(msTeamsShardIndex).toBeGreaterThanOrEqual(0);
expect(feishuShardIndex).toBeGreaterThanOrEqual(0);
expect(msTeamsShardIndex).not.toBe(feishuShardIndex);
});
it("treats extensions without tests as a no-op by default", () => {
const extensionId = findExtensionWithoutTests();
const stdout = runScript([extensionId]);
expect(stdout).toContain(`No tests found for ${bundledPluginRoot(extensionId)}.`);
expect(stdout).toContain("Skipping.");
});
});

View file

@ -0,0 +1,215 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
buildGroupedTestComparison,
buildGroupedTestReport,
renderGroupedTestComparison,
resolveGroupKey,
resolveTestArea,
} from "../../scripts/lib/test-group-report.mjs";
import {
parseTestGroupReportArgs,
resolveReportArtifactDirs,
} from "../../scripts/test-group-report.mjs";
describe("scripts/test-group-report grouping", () => {
it("groups repo files by stable product area", () => {
expect(resolveTestArea("extensions/discord/src/send.test.ts")).toBe("extensions/discord");
expect(resolveTestArea("src/commands/agent.test.ts")).toBe("src/commands");
expect(resolveTestArea("packages/plugin-sdk/src/index.test.ts")).toBe("packages/plugin-sdk");
expect(resolveTestArea("ui/src/ui/views/chat.test.ts")).toBe("ui/views");
expect(resolveTestArea("test/scripts/test-group-report.test.ts")).toBe("test/scripts");
});
it("supports folder and top-level grouping modes", () => {
expect(resolveGroupKey("src/commands/agent.test.ts", "folder")).toBe("src/commands");
expect(resolveGroupKey("extensions/browser/src/browser/pw.test.ts", "folder")).toBe(
"extensions/browser/src",
);
expect(resolveGroupKey("extensions/browser/src/browser/pw.test.ts", "top")).toBe("extensions");
});
});
describe("scripts/test-group-report aggregation", () => {
it("aggregates file durations by group and config", () => {
const report = buildGroupedTestReport({
groupBy: "area",
reports: [
{
config: "test/vitest/vitest.commands.config.ts",
report: {
testResults: [
{
name: path.join(process.cwd(), "src", "commands", "agent.test.ts"),
startTime: 100,
endTime: 700,
assertionResults: [{}, {}],
},
{
name: path.join(process.cwd(), "extensions", "discord", "src", "send.test.ts"),
startTime: 200,
endTime: 450,
assertionResults: [{}],
},
],
},
},
],
});
expect(report.totals).toEqual({ durationMs: 850, fileCount: 2, testCount: 3 });
expect(report.groups.map((group) => [group.key, group.durationMs])).toEqual([
["src/commands", 600],
["extensions/discord", 250],
]);
expect(report.configs).toMatchObject([
{
key: "commands",
durationMs: 850,
fileCount: 2,
testCount: 3,
},
]);
});
});
describe("scripts/test-group-report comparison", () => {
it("compares grouped reports by group, file, config, and run metrics", () => {
const comparison = buildGroupedTestComparison({
beforePath: "before.json",
afterPath: "after.json",
before: {
groupBy: "area",
totals: { durationMs: 1000, fileCount: 2, testCount: 4 },
groups: [
{ key: "src/commands", durationMs: 700, fileCount: 1, testCount: 2 },
{ key: "extensions/discord", durationMs: 300, fileCount: 1, testCount: 2 },
],
configs: [{ key: "commands", durationMs: 1000, fileCount: 2, testCount: 4 }],
topFiles: [
{
config: "commands",
file: "src/commands/agent.test.ts",
group: "src/commands",
durationMs: 700,
testCount: 2,
},
{
config: "commands",
file: "extensions/discord/src/send.test.ts",
group: "extensions/discord",
durationMs: 300,
testCount: 2,
},
],
runs: [
{
config: "test/vitest/vitest.commands.config.ts",
elapsedMs: 2000,
maxRssBytes: 1024 * 1024 * 100,
status: 0,
},
],
},
after: {
groupBy: "area",
totals: { durationMs: 900, fileCount: 2, testCount: 5 },
groups: [{ key: "src/commands", durationMs: 900, fileCount: 2, testCount: 5 }],
configs: [{ key: "commands", durationMs: 900, fileCount: 2, testCount: 5 }],
topFiles: [
{
config: "commands",
file: "src/commands/agent.test.ts",
group: "src/commands",
durationMs: 800,
testCount: 3,
},
{
config: "commands",
file: "src/commands/new.test.ts",
group: "src/commands",
durationMs: 100,
testCount: 2,
},
],
runs: [
{
config: "test/vitest/vitest.commands.config.ts",
elapsedMs: 1800,
maxRssBytes: 1024 * 1024 * 80,
status: 0,
},
],
},
});
expect(comparison.totals.delta).toEqual({ durationMs: -100, fileCount: 0, testCount: 1 });
expect(comparison.groups.find((group) => group.key === "src/commands")).toMatchObject({
delta: { durationMs: 200, fileCount: 1, testCount: 3 },
});
expect(
comparison.files.find((file) => file.file === "extensions/discord/src/send.test.ts"),
).toMatchObject({
status: "removed",
delta: { durationMs: -300, testCount: -2 },
});
expect(comparison.runs[0]).toMatchObject({
key: "commands",
delta: { elapsedMs: -200, maxRssBytes: -1024 * 1024 * 20 },
});
expect(renderGroupedTestComparison(comparison, { limit: 2, topFiles: 2 })).toContain(
"Top group regressions",
);
});
});
describe("scripts/test-group-report arg parsing", () => {
it("parses repeatable config and passthrough args", () => {
expect(
parseTestGroupReportArgs([
"--config",
"a.ts",
"--config",
"b.ts",
"--group-by",
"folder",
"--allow-failures",
"--",
"--maxWorkers=1",
]),
).toMatchObject({
allowFailures: true,
configs: ["a.ts", "b.ts"],
groupBy: "folder",
vitestArgs: ["--maxWorkers=1"],
});
});
it("parses compare mode", () => {
expect(
parseTestGroupReportArgs([
"--compare",
"before.json",
"after.json",
"--limit",
"5",
"--top-files",
"3",
]),
).toMatchObject({
compare: { before: "before.json", after: "after.json" },
limit: 5,
topFiles: 3,
});
});
});
describe("scripts/test-group-report artifact paths", () => {
it("keeps raw Vitest reports scoped to the output file stem", () => {
expect(resolveReportArtifactDirs(".artifacts/test-perf/baseline-before.json")).toEqual({
reportDir: path.join(".artifacts", "test-perf", "baseline-before", "vitest-json"),
logDir: path.join(".artifacts", "test-perf", "baseline-before", "logs"),
});
});
});

View file

@ -0,0 +1,41 @@
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach } from "vitest";
export function createScriptTestHarness() {
const tempDirs: string[] = [];
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function createTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
async function createTempDirAsync(prefix: string): Promise<string> {
const dir = await fsPromises.mkdtemp(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
function trackTempDir(dir: string): string {
tempDirs.push(dir);
return dir;
}
return {
createTempDir,
createTempDirAsync,
trackTempDir,
};
}

View file

@ -0,0 +1,95 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const SCRIPT_PATH = "scripts/test-install-sh-docker.sh";
const SMOKE_RUNNER_PATH = "scripts/docker/install-sh-smoke/run.sh";
describe("test-install-sh-docker", () => {
it("defaults local Apple Silicon smoke runs to native arm64 while keeping CI on amd64", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain("resolve_default_smoke_platform");
expect(script).toContain('printf "linux/amd64"');
expect(script).toContain('[[ "$host_os" == "Darwin" && "$host_arch" == "arm64" ]]');
expect(script).toContain('printf "linux/arm64"');
});
it("supports npm update package specs without a separate expected-version env", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain(
'UPDATE_EXPECT_VERSION="${OPENCLAW_INSTALL_SMOKE_UPDATE_EXPECT_VERSION:-}"',
);
expect(script).toContain('if [[ -z "$UPDATE_EXPECT_VERSION" ]]; then');
expect(script).toContain('UPDATE_EXPECT_VERSION="$packed_update_version"');
expect(script).toContain(
"packed update version ${packed_update_version} does not match expected ${UPDATE_EXPECT_VERSION}",
);
});
it("can reuse dist from the already-built root Docker smoke image", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain('UPDATE_DIST_IMAGE="${OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE:-}"');
expect(script).toContain("restore_local_dist_from_image");
expect(script).toContain('docker cp "${container_id}:/app/dist" "$ROOT_DIR/dist"');
expect(script).toContain('echo "==> Reuse local dist/ from Docker image: $image"');
});
it("prints package size audits for release smoke tarballs", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain("print_pack_audit");
expect(script).toContain("print_pack_delta_audit");
expect(script).toContain("==> Pack audit");
expect(script).toContain("==> Pack audit delta");
});
it("fails the update smoke when the candidate npm pack exceeds the release budget", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain("assert_pack_unpacked_size_budget");
expect(script).toContain('assert_pack_unpacked_size_budget "update" "$pack_json_file"');
expect(script).toContain('from "./scripts/lib/npm-pack-budget.mjs"');
expect(script).toContain("install smoke cannot verify pack budget");
});
it("writes the package dist inventory before packing ignore-scripts tarballs", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain("node --import tsx scripts/write-package-dist-inventory.ts");
expect(script).toContain("quiet_npm pack --ignore-scripts");
});
});
describe("install-sh smoke runner", () => {
it("wraps long npm/update operations with heartbeat and install-size audits", () => {
const script = readFileSync(SMOKE_RUNNER_PATH, "utf8");
expect(script).toContain(
'HEARTBEAT_INTERVAL="${OPENCLAW_INSTALL_SMOKE_HEARTBEAT_INTERVAL:-60}"',
);
expect(script).toContain(
'INSTALL_COMMAND_TIMEOUT="${OPENCLAW_INSTALL_SMOKE_COMMAND_TIMEOUT:-300}"',
);
expect(script).toContain("run_with_heartbeat");
expect(script).toContain("npm_install_global");
expect(script).toContain('timeout --foreground "${INSTALL_COMMAND_TIMEOUT}s"');
expect(script).toContain("==> Still running");
expect(script).toContain("print_install_audit");
expect(script).toContain('install -g "$@"');
expect(script).toContain("openclaw update --tag");
});
it("covers plain npm global installs and npm-driven updates", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
const runner = readFileSync(SMOKE_RUNNER_PATH, "utf8");
expect(script).toContain('SKIP_NPM_GLOBAL="${OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL:-0}"');
expect(script).toContain("==> Run direct npm global smoke");
expect(script).toContain("OPENCLAW_INSTALL_SMOKE_MODE=npm-global");
expect(runner).toContain("run_npm_global_smoke");
expect(runner).toContain("==> Direct npm global install candidate");
expect(runner).toContain("==> Direct npm global update candidate");
});
});

View file

@ -0,0 +1,22 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const SCRIPT_PATH = path.resolve(
import.meta.dirname,
"../../scripts/test-live-cli-backend-docker.sh",
);
function readForwardedDockerEnvVars(): string[] {
const script = fs.readFileSync(SCRIPT_PATH, "utf8");
return Array.from(script.matchAll(/-e\s+([A-Z0-9_]+)=/g), (match) => match[1] ?? "");
}
describe("scripts/test-live-cli-backend-docker.sh", () => {
it("forwards both fresh and resume CLI arg overrides into the Docker container", () => {
const forwardedVars = readForwardedDockerEnvVars();
expect(forwardedVars).toContain("OPENCLAW_LIVE_CLI_BACKEND_ARGS");
expect(forwardedVars).toContain("OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS");
});
});

View file

@ -0,0 +1,630 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
applyParallelVitestCachePaths,
buildFullSuiteVitestRunPlans,
buildVitestRunPlans,
shouldAcquireLocalHeavyCheckLock,
resolveChangedTargetArgs,
resolveParallelFullSuiteConcurrency,
} from "../../scripts/test-projects.test-support.mjs";
describe("scripts/test-projects changed-target routing", () => {
it("maps changed source files into scoped lane targets", () => {
expect(
resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [
"src/shared/string-normalization.ts",
"src/utils/provider-utils.ts",
]),
).toEqual(["src/shared/string-normalization.ts", "src/utils/provider-utils.ts"]);
});
it("keeps the broad changed run for Vitest wiring edits", () => {
expect(
resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [
"test/vitest/vitest.shared.config.ts",
"src/utils/provider-utils.ts",
]),
).toBeNull();
});
it("ignores changed files that cannot map to test lanes", () => {
expect(
resolveChangedTargetArgs(["--changed", "origin/main"], process.cwd(), () => [
"docs/help/testing.md",
]),
).toBeNull();
});
it("narrows default-lane changed source files to include globs", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"packages/sdk/src/index.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit.config.ts",
forwardedArgs: [],
includePatterns: ["packages/sdk/src/**/*.test.ts"],
watchMode: false,
},
]);
});
it("routes changed utils and shared files to their light scoped lanes", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/shared/string-normalization.ts",
"src/utils/provider-utils.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/shared/string-normalization.test.ts"],
watchMode: false,
},
{
config: "test/vitest/vitest.utils.config.ts",
forwardedArgs: [],
includePatterns: ["src/utils/**/*.test.ts"],
watchMode: false,
},
]);
});
it("routes explicit plugin-sdk light tests to the lighter plugin-sdk lane", () => {
const plans = buildVitestRunPlans(["src/plugin-sdk/temp-path.test.ts"], process.cwd());
expect(plans).toEqual([
{
config: "test/vitest/vitest.plugin-sdk-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/temp-path.test.ts"],
watchMode: false,
},
]);
});
it("routes explicit commands light tests to the lighter commands lane", () => {
const plans = buildVitestRunPlans(["src/commands/status-json-runtime.test.ts"], process.cwd());
expect(plans).toEqual([
{
config: "test/vitest/vitest.commands-light.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/status-json-runtime.test.ts"],
watchMode: false,
},
]);
});
it("routes unit-fast light tests to the cache-friendly unit-fast lane", () => {
const plans = buildVitestRunPlans(
["src/commands/status-overview-values.test.ts"],
process.cwd(),
);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/status-overview-values.test.ts"],
watchMode: false,
},
]);
});
it("routes changed plugin-sdk source allowlist files to sibling light tests", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/plugin-sdk/provider-entry.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/provider-entry.test.ts"],
watchMode: false,
},
]);
});
it("routes changed commands source allowlist files to sibling light tests", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/commands/status-overview-values.ts",
"src/commands/gateway-status/helpers.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.unit-fast.config.ts",
forwardedArgs: [],
includePatterns: [
"src/commands/status-overview-values.test.ts",
"src/commands/gateway-status/helpers.test.ts",
],
watchMode: false,
},
]);
});
it("keeps non-allowlisted plugin-sdk source files on the heavy lane", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/plugin-sdk/facade-runtime.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.plugin-sdk.config.ts",
forwardedArgs: [],
includePatterns: ["src/plugin-sdk/**/*.test.ts"],
watchMode: false,
},
]);
});
it("keeps non-allowlisted commands source files on the heavy lane", () => {
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
"src/commands/channels.add.ts",
]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.commands.config.ts",
forwardedArgs: [],
includePatterns: ["src/commands/**/*.test.ts"],
watchMode: false,
},
]);
});
it.each([
"src/gateway/gateway.test.ts",
"src/gateway/server.startup-matrix-migration.integration.test.ts",
"src/gateway/sessions-history-http.test.ts",
])("routes gateway integration fixture %s to the e2e lane", (target) => {
const plans = buildVitestRunPlans([target], process.cwd());
expect(plans).toEqual([
{
config: "test/vitest/vitest.e2e.config.ts",
forwardedArgs: [target],
includePatterns: null,
watchMode: false,
},
]);
});
});
describe("scripts/test-projects local heavy-check lock", () => {
it("skips the lock for a single scoped tooling run", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/committer.test.ts"],
watchMode: false,
},
],
process.env,
),
).toBe(false);
});
it("keeps the lock for non-tooling runs", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.unit.config.ts",
includePatterns: ["src/infra/vitest-config.test.ts"],
watchMode: false,
},
],
process.env,
),
).toBe(true);
});
it("allows forcing the lock back on", () => {
expect(
shouldAcquireLocalHeavyCheckLock(
[
{
config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/committer.test.ts"],
watchMode: false,
},
],
{
...process.env,
OPENCLAW_TEST_PROJECTS_FORCE_LOCK: "1",
},
),
).toBe(true);
});
});
describe("scripts/test-projects full-suite sharding", () => {
it("uses the large host-aware local profile on roomy local hosts", () => {
expect(
resolveParallelFullSuiteConcurrency(
61,
{},
{
cpuCount: 14,
loadAverage1m: 0,
totalMemoryBytes: 48 * 1024 ** 3,
},
),
).toBe(10);
});
it("keeps CI full-suite runs serial even on roomy hosts", () => {
expect(
resolveParallelFullSuiteConcurrency(
61,
{
CI: "true",
},
{
cpuCount: 14,
loadAverage1m: 0,
totalMemoryBytes: 48 * 1024 ** 3,
},
),
).toBe(1);
});
it("keeps explicit parallel overrides ahead of the host-aware profile", () => {
expect(
resolveParallelFullSuiteConcurrency(
61,
{
OPENCLAW_TEST_PROJECTS_PARALLEL: "3",
},
{
cpuCount: 14,
loadAverage1m: 0,
totalMemoryBytes: 48 * 1024 ** 3,
},
),
).toBe(3);
});
it("splits untargeted runs into fixed core shards and per-extension configs", () => {
const previousParallel = process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
const previousSerial = process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
delete process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD;
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
process.env.OPENCLAW_TEST_PROJECTS_SERIAL = "1";
try {
expect(buildFullSuiteVitestRunPlans([], process.cwd()).map((plan) => plan.config)).toEqual([
"test/vitest/vitest.full-core-unit-fast.config.ts",
"test/vitest/vitest.full-core-unit-src.config.ts",
"test/vitest/vitest.full-core-unit-security.config.ts",
"test/vitest/vitest.full-core-unit-ui.config.ts",
"test/vitest/vitest.full-core-unit-support.config.ts",
"test/vitest/vitest.full-core-support-boundary.config.ts",
"test/vitest/vitest.full-core-contracts.config.ts",
"test/vitest/vitest.full-core-bundled.config.ts",
"test/vitest/vitest.full-core-runtime.config.ts",
"test/vitest/vitest.full-agentic.config.ts",
"test/vitest/vitest.full-auto-reply.config.ts",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channels.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",
"test/vitest/vitest.extension-irc.config.ts",
"test/vitest/vitest.extension-mattermost.config.ts",
"test/vitest/vitest.extension-matrix.config.ts",
"test/vitest/vitest.extension-memory.config.ts",
"test/vitest/vitest.extension-messaging.config.ts",
"test/vitest/vitest.extension-msteams.config.ts",
"test/vitest/vitest.extension-providers.config.ts",
"test/vitest/vitest.extension-telegram.config.ts",
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
]);
} finally {
if (previousParallel === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL = previousParallel;
}
if (previousSerial === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_SERIAL = previousSerial;
}
}
});
it("expands untargeted local runs to leaf project configs by default", () => {
const previousLeafShards = process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
const previousParallel = process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
const previousSerial = process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
const previousCi = process.env.CI;
const previousActions = process.env.GITHUB_ACTIONS;
const previousVitestMaxWorkers = process.env.OPENCLAW_VITEST_MAX_WORKERS;
const previousTestWorkers = process.env.OPENCLAW_TEST_WORKERS;
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
delete process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
delete process.env.CI;
delete process.env.GITHUB_ACTIONS;
delete process.env.OPENCLAW_VITEST_MAX_WORKERS;
delete process.env.OPENCLAW_TEST_WORKERS;
try {
const configs = buildFullSuiteVitestRunPlans([], process.cwd()).map((plan) => plan.config);
expect(configs).toContain("test/vitest/vitest.gateway-server.config.ts");
expect(configs).toContain("test/vitest/vitest.extension-telegram.config.ts");
expect(configs).not.toContain("test/vitest/vitest.full-agentic.config.ts");
expect(configs).not.toContain("test/vitest/vitest.full-core-unit-fast.config.ts");
} finally {
if (previousLeafShards === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
} else {
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = previousLeafShards;
}
if (previousParallel === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL = previousParallel;
}
if (previousSerial === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_SERIAL = previousSerial;
}
if (previousCi === undefined) {
delete process.env.CI;
} else {
process.env.CI = previousCi;
}
if (previousActions === undefined) {
delete process.env.GITHUB_ACTIONS;
} else {
process.env.GITHUB_ACTIONS = previousActions;
}
if (previousVitestMaxWorkers === undefined) {
delete process.env.OPENCLAW_VITEST_MAX_WORKERS;
} else {
process.env.OPENCLAW_VITEST_MAX_WORKERS = previousVitestMaxWorkers;
}
if (previousTestWorkers === undefined) {
delete process.env.OPENCLAW_TEST_WORKERS;
} else {
process.env.OPENCLAW_TEST_WORKERS = previousTestWorkers;
}
}
});
it("can skip the aggregate extension shard when CI runs dedicated extension shards", () => {
const previous = process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD;
const previousParallel = process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
const previousSerial = process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
process.env.OPENCLAW_TEST_PROJECTS_SERIAL = "1";
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD = "1";
try {
const configs = buildFullSuiteVitestRunPlans([], process.cwd()).map((plan) => plan.config);
expect(configs).not.toContain("test/vitest/vitest.full-extensions.config.ts");
expect(configs).toContain("test/vitest/vitest.full-auto-reply.config.ts");
} finally {
if (previous === undefined) {
delete process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD;
} else {
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD = previous;
}
if (previousParallel === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL = previousParallel;
}
if (previousSerial === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_SERIAL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_SERIAL = previousSerial;
}
}
});
it("can expand full-suite shards to project configs for perf experiments", () => {
const previous = process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = "1";
let plans: ReturnType<typeof buildFullSuiteVitestRunPlans>;
try {
plans = buildFullSuiteVitestRunPlans([], process.cwd());
} finally {
if (previous === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
} else {
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = previous;
}
}
expect(plans.map((plan) => plan.config)).toEqual([
"test/vitest/vitest.unit-fast.config.ts",
"test/vitest/vitest.unit-src.config.ts",
"test/vitest/vitest.unit-security.config.ts",
"test/vitest/vitest.unit-ui.config.ts",
"test/vitest/vitest.unit-support.config.ts",
"test/vitest/vitest.boundary.config.ts",
"test/vitest/vitest.tooling.config.ts",
"test/vitest/vitest.contracts.config.ts",
"test/vitest/vitest.bundled.config.ts",
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.utils.config.ts",
"test/vitest/vitest.wizard.config.ts",
"test/vitest/vitest.gateway-core.config.ts",
"test/vitest/vitest.gateway-client.config.ts",
"test/vitest/vitest.gateway-methods.config.ts",
"test/vitest/vitest.gateway-server.config.ts",
"test/vitest/vitest.cli.config.ts",
"test/vitest/vitest.commands-light.config.ts",
"test/vitest/vitest.commands.config.ts",
"test/vitest/vitest.agents.config.ts",
"test/vitest/vitest.daemon.config.ts",
"test/vitest/vitest.plugin-sdk-light.config.ts",
"test/vitest/vitest.plugin-sdk.config.ts",
"test/vitest/vitest.plugins.config.ts",
"test/vitest/vitest.channels.config.ts",
"test/vitest/vitest.auto-reply-core.config.ts",
"test/vitest/vitest.auto-reply-top-level.config.ts",
"test/vitest/vitest.auto-reply-reply.config.ts",
"test/vitest/vitest.extension-acpx.config.ts",
"test/vitest/vitest.extension-bluebubbles.config.ts",
"test/vitest/vitest.extension-channels.config.ts",
"test/vitest/vitest.extension-diffs.config.ts",
"test/vitest/vitest.extension-feishu.config.ts",
"test/vitest/vitest.extension-irc.config.ts",
"test/vitest/vitest.extension-mattermost.config.ts",
"test/vitest/vitest.extension-matrix.config.ts",
"test/vitest/vitest.extension-memory.config.ts",
"test/vitest/vitest.extension-messaging.config.ts",
"test/vitest/vitest.extension-msteams.config.ts",
"test/vitest/vitest.extension-providers.config.ts",
"test/vitest/vitest.extension-telegram.config.ts",
"test/vitest/vitest.extension-voice-call.config.ts",
"test/vitest/vitest.extension-whatsapp.config.ts",
"test/vitest/vitest.extension-zalo.config.ts",
"test/vitest/vitest.extension-browser.config.ts",
"test/vitest/vitest.extension-qa.config.ts",
"test/vitest/vitest.extension-media.config.ts",
"test/vitest/vitest.extension-misc.config.ts",
]);
expect(plans).toEqual(
plans.map((plan) => ({
config: plan.config,
forwardedArgs: [],
includePatterns: null,
watchMode: false,
})),
);
});
it("skips extension project configs when leaf sharding and the aggregate extension shard is disabled", () => {
const previousLeafShards = process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
const previousSkipExtensions = process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD;
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = "1";
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD = "1";
try {
const configs = buildFullSuiteVitestRunPlans([], process.cwd()).map((plan) => plan.config);
expect(configs).not.toContain("test/vitest/vitest.extensions.config.ts");
expect(configs).not.toContain("test/vitest/vitest.extension-providers.config.ts");
expect(configs).toContain("test/vitest/vitest.auto-reply-reply.config.ts");
} finally {
if (previousLeafShards === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
} else {
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = previousLeafShards;
}
if (previousSkipExtensions === undefined) {
delete process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD;
} else {
process.env.OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD = previousSkipExtensions;
}
}
});
it("expands full-suite shards before running them in parallel", () => {
const previousLeafShards = process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
const previousParallel = process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL = "6";
try {
const configs = buildFullSuiteVitestRunPlans([], process.cwd()).map((plan) => plan.config);
expect(configs).toContain("test/vitest/vitest.extension-telegram.config.ts");
expect(configs).not.toContain("test/vitest/vitest.full-extensions.config.ts");
} finally {
if (previousLeafShards === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS;
} else {
process.env.OPENCLAW_TEST_PROJECTS_LEAF_SHARDS = previousLeafShards;
}
if (previousParallel === undefined) {
delete process.env.OPENCLAW_TEST_PROJECTS_PARALLEL;
} else {
process.env.OPENCLAW_TEST_PROJECTS_PARALLEL = previousParallel;
}
}
});
it("keeps untargeted watch mode on the native root config", () => {
expect(buildFullSuiteVitestRunPlans(["--watch"], process.cwd())).toEqual([
{
config: "vitest.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: true,
},
]);
});
});
describe("scripts/test-projects parallel cache paths", () => {
it("assigns isolated Vitest fs-module cache paths per parallel shard", () => {
const specs = applyParallelVitestCachePaths(
[
{ config: "test/vitest/vitest.gateway.config.ts", env: {}, pnpmArgs: [] },
{ config: "test/vitest/vitest.extension-matrix.config.ts", env: {}, pnpmArgs: [] },
],
{ cwd: "/repo", env: {} },
);
expect(specs.map((spec) => spec.env)).toEqual([
{
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: path.join(
"/repo",
"node_modules",
".experimental-vitest-cache",
"0-test-vitest-vitest.gateway.config.ts",
),
},
{
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: path.join(
"/repo",
"node_modules",
".experimental-vitest-cache",
"1-test-vitest-vitest.extension-matrix.config.ts",
),
},
]);
});
it("keeps an explicit global cache path", () => {
const [spec] = applyParallelVitestCachePaths(
[{ config: "test/vitest/vitest.gateway.config.ts", env: {}, pnpmArgs: [] }],
{ cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } },
);
expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
});
});

View file

@ -0,0 +1,121 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
collectVitestFileDurations,
normalizeTrackedRepoPath,
tryReadJsonFile,
} from "../../scripts/test-report-utils.mjs";
const { spawnSyncMock } = vi.hoisted(() => ({
spawnSyncMock: vi.fn(),
}));
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
spawnSync: spawnSyncMock,
};
});
describe("scripts/test-report-utils normalizeTrackedRepoPath", () => {
it("normalizes repo-local absolute paths to repo-relative slash paths", () => {
const absoluteFile = path.join(process.cwd(), "src", "tools", "example.test.ts");
expect(normalizeTrackedRepoPath(absoluteFile)).toBe("src/tools/example.test.ts");
});
it("preserves external absolute paths as normalized absolute paths", () => {
const externalFile = path.join(path.parse(process.cwd()).root, "tmp", "outside.test.ts");
expect(normalizeTrackedRepoPath(externalFile)).toBe(externalFile.split(path.sep).join("/"));
});
});
describe("scripts/test-report-utils collectVitestFileDurations", () => {
it("extracts per-file durations and applies file normalization", () => {
const report = {
testResults: [
{
name: path.join(process.cwd(), "src", "alpha.test.ts"),
startTime: 100,
endTime: 460,
assertionResults: [{}, {}],
},
{
name: "src/zero.test.ts",
startTime: 300,
endTime: 300,
assertionResults: [{}],
},
],
};
expect(collectVitestFileDurations(report, normalizeTrackedRepoPath)).toEqual([
{
file: "src/alpha.test.ts",
durationMs: 360,
testCount: 2,
},
]);
});
});
describe("scripts/test-report-utils tryReadJsonFile", () => {
it("returns the fallback when the file is missing", () => {
const missingPath = path.join(os.tmpdir(), `openclaw-missing-${Date.now()}.json`);
expect(tryReadJsonFile(missingPath, { ok: true })).toEqual({ ok: true });
});
it("reads valid JSON files", () => {
const tempPath = path.join(os.tmpdir(), `openclaw-json-${Date.now()}.json`);
fs.writeFileSync(tempPath, JSON.stringify({ ok: true }));
try {
expect(tryReadJsonFile(tempPath, null)).toEqual({ ok: true });
} finally {
fs.unlinkSync(tempPath);
}
});
});
describe("scripts/test-report-utils runVitestJsonReport", () => {
beforeEach(() => {
vi.resetModules();
spawnSyncMock.mockReset();
});
it("launches Vitest through pnpm exec", async () => {
spawnSyncMock.mockReturnValue({ status: 0 });
const reportPath = path.join(os.tmpdir(), `openclaw-vitest-json-${Date.now()}.json`);
const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs");
expect(
runVitestJsonReport({
config: "test/vitest/vitest.unit.config.ts",
reportPath,
}),
).toBe(reportPath);
expect(spawnSyncMock).toHaveBeenCalledWith(
"pnpm",
[
"exec",
"vitest",
"run",
"--config",
"test/vitest/vitest.unit.config.ts",
"--reporter=json",
"--outputFile",
reportPath,
],
expect.objectContaining({
stdio: "inherit",
env: process.env,
}),
);
});
});

View file

@ -0,0 +1,51 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import { resolveRepoRoot } from "../../scripts/lib/ts-guard-utils.mjs";
/**
* Regression tests for resolveRepoRoot().
*
* The original implementation went up exactly two levels from the caller's
* import.meta.url, which broke for scripts at scripts/*.mjs (one level below
* root) — it overshot to the repo's parent directory.
*/
describe("resolveRepoRoot", () => {
it("resolves correctly from a scripts/lib/*.mjs path (two levels below root)", () => {
const fakeUrl = pathToFileURL(path.resolve("scripts", "lib", "some-guard-utils.mjs")).href;
const root = resolveRepoRoot(fakeUrl);
expect(existsSync(path.join(root, ".git"))).toBe(true);
expect(existsSync(path.join(root, "package.json"))).toBe(true);
});
it("resolves correctly from a scripts/*.mjs path (one level below root)", () => {
const fakeUrl = pathToFileURL(path.resolve("scripts", "check-no-raw-channel-fetch.mjs")).href;
const root = resolveRepoRoot(fakeUrl);
expect(existsSync(path.join(root, ".git"))).toBe(true);
expect(existsSync(path.join(root, "package.json"))).toBe(true);
});
it("resolves correctly from a deeply nested extension path", () => {
const fakeUrl = pathToFileURL(
path.resolve("extensions", "qqbot", "src", "utils", "hypothetical.mjs"),
).href;
const root = resolveRepoRoot(fakeUrl);
expect(existsSync(path.join(root, ".git"))).toBe(true);
expect(existsSync(path.join(root, "package.json"))).toBe(true);
});
it("all caller depths resolve to the same root", () => {
const fromLib = resolveRepoRoot(pathToFileURL(path.resolve("scripts", "lib", "a.mjs")).href);
const fromScripts = resolveRepoRoot(pathToFileURL(path.resolve("scripts", "b.mjs")).href);
const fromExtension = resolveRepoRoot(
pathToFileURL(path.resolve("extensions", "qqbot", "c.mjs")).href,
);
expect(fromLib).toBe(fromScripts);
expect(fromScripts).toBe(fromExtension);
});
});

View file

@ -0,0 +1,154 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { analyzeTopology } from "../../scripts/lib/ts-topology/analyze.js";
import { renderTextReport } from "../../scripts/lib/ts-topology/reports.js";
import { createFilesystemPublicSurfaceScope } from "../../scripts/lib/ts-topology/scope.js";
import { main } from "../../scripts/ts-topology.ts";
import { createCapturedIo } from "../helpers/captured-io.js";
const repoRoot = path.join(process.cwd(), "test", "fixtures", "ts-topology", "basic");
function buildFixtureScope() {
return createFilesystemPublicSurfaceScope(repoRoot, {
id: "custom",
entrypointRoot: "src/public",
importPrefix: "fixture-sdk",
});
}
describe("ts-topology", () => {
it("collapses canonical symbols exported by multiple public subpaths", () => {
const envelope = analyzeTopology({
repoRoot,
scope: buildFixtureScope(),
report: "public-surface-usage",
});
const sharedThing = envelope.records.find((record) =>
record.exportNames.includes("sharedThing"),
);
expect(sharedThing).toMatchObject({
declarationPath: "src/lib/shared.ts",
declarationLine: 1,
productionExtensions: ["alpha", "beta"],
productionPackages: ["src"],
productionOwners: ["extension:alpha", "extension:beta", "src"],
});
expect(sharedThing?.publicSpecifiers).toEqual(["fixture-sdk", "fixture-sdk/extra"]);
});
it("counts renamed imports, namespace imports, type-only imports, and test-only consumers", () => {
const envelope = analyzeTopology({
repoRoot,
scope: buildFixtureScope(),
report: "public-surface-usage",
});
const aliasedThing = envelope.records.find((record) =>
record.exportNames.includes("aliasedThing"),
);
const sharedType = envelope.records.find((record) => record.exportNames.includes("SharedType"));
const testOnlyThing = envelope.records.find((record) =>
record.exportNames.includes("testOnlyThing"),
);
expect(aliasedThing?.productionRefCount).toBe(1);
expect(sharedType).toMatchObject({
isTypeOnlyCandidate: true,
productionExtensions: ["alpha", "beta"],
productionRefCount: 2,
});
expect(testOnlyThing).toMatchObject({
productionRefCount: 0,
testRefCount: 1,
testConsumers: ["tests/public.test.ts"],
});
});
it("surfaces single-owner shared and unused reports correctly", () => {
const singleOwner = analyzeTopology({
repoRoot,
scope: buildFixtureScope(),
report: "single-owner-shared",
});
const unused = analyzeTopology({
repoRoot,
scope: buildFixtureScope(),
report: "unused-public-surface",
});
expect(singleOwner.records.map((record) => record.exportNames[0])).toContain(
"singleOwnerHelper",
);
expect(singleOwner.records.map((record) => record.exportNames[0])).not.toContain("sharedThing");
expect(unused.records.map((record) => record.exportNames[0])).toEqual(["unusedThing"]);
});
it("renders stable text summaries for the public-surface report", () => {
const envelope = analyzeTopology({
repoRoot,
scope: buildFixtureScope(),
report: "public-surface-usage",
limit: 3,
});
expect(renderTextReport(envelope, 3)).toMatchInlineSnapshot(`
"Scope: custom
Public exports analyzed: 6
Production-used exports: 4
Single-owner shared exports: 2
Unused public exports: 1
Top 2 candidate-to-move exports:
- fixture-sdk:aliasedThing -> src/lib/shared.ts:9 (prodRefs=1, owners=extension:alpha, sharedness=35, move=85)
- fixture-sdk:singleOwnerHelper -> src/lib/shared.ts:5 (prodRefs=1, owners=extension:alpha, sharedness=35, move=85)
Top 1 duplicated public exports:
- fixture-sdk:sharedThing via fixture-sdk, fixture-sdk/extra (src/lib/shared.ts:1)"
`);
});
it("emits stable JSON and filtered report output through the CLI", async () => {
const captured = createCapturedIo();
const jsonExit = await main(
[
"--scope=custom",
"--entrypoint-root=src/public",
"--import-prefix=fixture-sdk",
"--repo-root=test/fixtures/ts-topology/basic",
"--report=single-owner-shared",
"--json",
],
captured.io,
);
expect(jsonExit).toBe(0);
const payload = JSON.parse(captured.readStdout());
expect(payload.report).toBe("single-owner-shared");
expect(
payload.records.map((record: { exportNames: string[] }) => record.exportNames[0]),
).toEqual(["aliasedThing", "singleOwnerHelper"]);
const textCaptured = createCapturedIo();
const textExit = await main(
[
"--scope=custom",
"--entrypoint-root=src/public",
"--import-prefix=fixture-sdk",
"--repo-root=test/fixtures/ts-topology/basic",
"--report=consumer-topology",
"--limit=2",
],
textCaptured.io,
);
expect(textExit).toBe(0);
expect(textCaptured.readStdout()).toMatchInlineSnapshot(`
"Scope: custom
Records with consumers: 5
Top 2 consumer-topology records:
- fixture-sdk:sharedThing prod=3 test=0 internal=0
- fixture-sdk:SharedType prod=2 test=0 internal=0
"
`);
});
});

View file

@ -0,0 +1,105 @@
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
pruneSourceCheckoutBundledPluginNodeModules,
pruneStaleRootChunkFiles,
resolveTsdownBuildInvocation,
} from "../../scripts/tsdown-build.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
const { createTempDir } = createScriptTestHarness();
describe("resolveTsdownBuildInvocation", () => {
it("routes Windows tsdown builds through the pnpm runner instead of shell=true", () => {
const result = resolveTsdownBuildInvocation({
platform: "win32",
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
env: {},
});
expect(result).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: [
"C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
"exec",
"tsdown",
"--config-loader",
"unrun",
"--logLevel",
"warn",
],
options: {
encoding: "utf8",
stdio: "pipe",
shell: false,
windowsVerbatimArguments: undefined,
env: {},
},
});
});
it("keeps source-checkout prune best-effort", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const rmSync = vi.spyOn(fs, "rmSync");
rmSync.mockImplementation(() => {
throw new Error("locked");
});
expect(() =>
pruneSourceCheckoutBundledPluginNodeModules({
cwd: process.cwd(),
}),
).not.toThrow();
expect(warn).toHaveBeenCalledWith(
"tsdown: could not prune bundled plugin source node_modules: Error: locked",
);
warn.mockRestore();
rmSync.mockRestore();
});
it("prunes stale hashed root chunk files but keeps stable aliases and nested assets", async () => {
const rootDir = createTempDir("openclaw-tsdown-build-");
const distDir = path.join(rootDir, "dist");
const distRuntimeDir = path.join(rootDir, "dist-runtime");
await fsPromises.mkdir(path.join(distDir, "control-ui"), { recursive: true });
await fsPromises.mkdir(distRuntimeDir, { recursive: true });
await fsPromises.writeFile(path.join(distDir, "delegate-BPjCe4gC.js"), "old delegate\n");
await fsPromises.writeFile(path.join(distDir, "compact.runtime-2DiEmVcA.js"), "old runtime\n");
await fsPromises.writeFile(path.join(distDir, "compact.runtime.js"), "stable alias\n");
await fsPromises.writeFile(path.join(distDir, "entry.js"), "entry\n");
await fsPromises.writeFile(path.join(distDir, "control-ui", "index.html"), "asset\n");
await fsPromises.writeFile(
path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js"),
"old runtime\n",
);
await fsPromises.writeFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "alias\n");
pruneStaleRootChunkFiles({ cwd: rootDir });
await expect(
fsPromises.readFile(path.join(distDir, "compact.runtime.js"), "utf8"),
).resolves.toBe("stable alias\n");
await expect(fsPromises.readFile(path.join(distDir, "entry.js"), "utf8")).resolves.toBe(
"entry\n",
);
await expect(
fsPromises.readFile(path.join(distDir, "control-ui", "index.html"), "utf8"),
).resolves.toBe("asset\n");
await expect(
fsPromises.readFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "utf8"),
).resolves.toBe("alias\n");
await expect(fsPromises.stat(path.join(distDir, "delegate-BPjCe4gC.js"))).rejects.toThrow();
await expect(
fsPromises.stat(path.join(distDir, "compact.runtime-2DiEmVcA.js")),
).rejects.toThrow();
await expect(
fsPromises.stat(path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js")),
).rejects.toThrow();
});
});

View file

@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { assertSafeWindowsShellArgs, shouldUseShellForCommand } from "../../scripts/ui.js";
describe("scripts/ui windows spawn behavior", () => {
it("enables shell for Windows command launchers that require cmd.exe", () => {
expect(
shouldUseShellForCommand("C:\\Users\\dev\\AppData\\Local\\pnpm\\pnpm.CMD", "win32"),
).toBe(true);
expect(shouldUseShellForCommand("C:\\tools\\pnpm.bat", "win32")).toBe(true);
});
it("does not enable shell for non-shell launchers", () => {
expect(shouldUseShellForCommand("C:\\Program Files\\nodejs\\node.exe", "win32")).toBe(false);
expect(shouldUseShellForCommand("/usr/local/bin/pnpm", "linux")).toBe(false);
});
it("allows safe forwarded args when shell mode is required on Windows", () => {
expect(() =>
assertSafeWindowsShellArgs(["run", "build", "--filter", "@openclaw/ui"], "win32"),
).not.toThrow();
});
it("rejects dangerous forwarded args when shell mode is required on Windows", () => {
expect(() => assertSafeWindowsShellArgs(["run", "build", "evil&calc"], "win32")).toThrow(
/unsafe windows shell argument/i,
);
expect(() => assertSafeWindowsShellArgs(["run", "build", "%PATH%"], "win32")).toThrow(
/unsafe windows shell argument/i,
);
});
it("does not reject args on non-windows platforms", () => {
expect(() => assertSafeWindowsShellArgs(["contains&metacharacters"], "linux")).not.toThrow();
});
});

View file

@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import {
resolveLocalFullSuiteProfile,
resolveLocalVitestScheduling,
shouldUseLargeLocalFullSuiteProfile,
} from "../../scripts/lib/vitest-local-scheduling.mjs";
describe("vitest local full-suite profile", () => {
it("selects the large local profile on roomy hosts that are not throttled", () => {
const env = {};
const hostInfo = {
cpuCount: 14,
loadAverage1m: 0,
totalMemoryBytes: 48 * 1024 ** 3,
};
expect(resolveLocalVitestScheduling(env, hostInfo, "threads")).toEqual({
maxWorkers: 6,
fileParallelism: true,
throttledBySystem: false,
});
expect(shouldUseLargeLocalFullSuiteProfile(env, hostInfo)).toBe(true);
expect(resolveLocalFullSuiteProfile(env, hostInfo)).toEqual({
shardParallelism: 10,
vitestMaxWorkers: 2,
});
});
it("keeps the smaller local profile when the host is already throttled", () => {
const hostInfo = {
cpuCount: 14,
loadAverage1m: 14,
totalMemoryBytes: 48 * 1024 ** 3,
};
expect(shouldUseLargeLocalFullSuiteProfile({}, hostInfo)).toBe(false);
expect(resolveLocalFullSuiteProfile({}, hostInfo)).toEqual({
shardParallelism: 4,
vitestMaxWorkers: 1,
});
});
it("never selects the large local profile in CI", () => {
const hostInfo = {
cpuCount: 14,
loadAverage1m: 0,
totalMemoryBytes: 48 * 1024 ** 3,
};
expect(shouldUseLargeLocalFullSuiteProfile({ CI: "true" }, hostInfo)).toBe(false);
expect(resolveLocalFullSuiteProfile({ CI: "true" }, hostInfo)).toEqual({
shardParallelism: 4,
vitestMaxWorkers: 1,
});
});
});

View file

@ -0,0 +1,99 @@
import { describe, expect, it, vi } from "vitest";
import {
forwardSignalToVitestProcessGroup,
installVitestProcessGroupCleanup,
resolveVitestProcessGroupSignalTarget,
shouldUseDetachedVitestProcessGroup,
} from "../../scripts/vitest-process-group.mjs";
describe("vitest process group helpers", () => {
it("uses detached process groups on non-Windows hosts", () => {
expect(shouldUseDetachedVitestProcessGroup("darwin")).toBe(true);
expect(shouldUseDetachedVitestProcessGroup("linux")).toBe(true);
expect(shouldUseDetachedVitestProcessGroup("win32")).toBe(false);
});
it("targets the process group on Unix and the direct pid on Windows", () => {
expect(resolveVitestProcessGroupSignalTarget({ childPid: 4200, platform: "darwin" })).toBe(
-4200,
);
expect(resolveVitestProcessGroupSignalTarget({ childPid: 4200, platform: "win32" })).toBe(4200);
expect(resolveVitestProcessGroupSignalTarget({ childPid: undefined, platform: "darwin" })).toBe(
null,
);
});
it("forwards signals to the computed target and ignores cleanup races", () => {
const kill = vi.fn();
expect(
forwardSignalToVitestProcessGroup({
child: { pid: 4200 },
signal: "SIGTERM",
platform: "darwin",
kill,
}),
).toBe(true);
expect(kill).toHaveBeenCalledWith(-4200, "SIGTERM");
kill.mockImplementationOnce(() => {
const error = new Error("gone") as NodeJS.ErrnoException;
error.code = "ESRCH";
throw error;
});
expect(
forwardSignalToVitestProcessGroup({
child: { pid: 4200 },
signal: "SIGTERM",
platform: "darwin",
kill,
}),
).toBe(false);
kill.mockImplementationOnce(() => {
const error = new Error("permission race") as NodeJS.ErrnoException;
error.code = "EPERM";
throw error;
});
expect(
forwardSignalToVitestProcessGroup({
child: { pid: 4200 },
signal: "SIGTERM",
platform: "darwin",
kill,
}),
).toBe(false);
});
it("installs and removes process cleanup listeners", () => {
const listeners = new Map<string, Set<() => void>>();
const fakeProcess = {
on(event: string, handler: () => void) {
const set = listeners.get(event) ?? new Set();
set.add(handler);
listeners.set(event, set);
},
off(event: string, handler: () => void) {
listeners.get(event)?.delete(handler);
},
};
const kill = vi.fn();
const teardown = installVitestProcessGroupCleanup({
child: { pid: 4200 },
processObject: fakeProcess as unknown as NodeJS.Process,
platform: "darwin",
kill,
});
expect(listeners.get("SIGINT")?.size).toBe(1);
expect(listeners.get("SIGTERM")?.size).toBe(1);
expect(listeners.get("exit")?.size).toBe(1);
listeners.get("SIGTERM")?.values().next().value?.();
expect(kill).toHaveBeenCalledWith(-4200, "SIGTERM");
teardown();
expect(listeners.get("SIGINT")?.size ?? 0).toBe(0);
expect(listeners.get("SIGTERM")?.size ?? 0).toBe(0);
expect(listeners.get("exit")?.size ?? 0).toBe(0);
});
});

View file

@ -0,0 +1,42 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { writeCliStartupMetadata } from "../../scripts/write-cli-startup-metadata.ts";
import { createScriptTestHarness } from "./test-helpers.js";
describe("write-cli-startup-metadata", () => {
const { createTempDir } = createScriptTestHarness();
it("writes startup metadata with populated root help text when dist falls back to source rendering", async () => {
const tempRoot = createTempDir("openclaw-startup-metadata-");
const distDir = path.join(tempRoot, "dist");
const extensionsDir = path.join(tempRoot, "extensions");
const outputPath = path.join(distDir, "cli-startup-metadata.json");
mkdirSync(distDir, { recursive: true });
mkdirSync(path.join(extensionsDir, "matrix"), { recursive: true });
writeFileSync(
path.join(extensionsDir, "matrix", "package.json"),
JSON.stringify({
openclaw: {
channel: {
id: "matrix",
order: 120,
label: "Matrix",
},
},
}),
"utf8",
);
await writeCliStartupMetadata({ distDir, outputPath, extensionsDir });
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
channelOptions: string[];
rootHelpText: string;
};
expect(written.channelOptions).toContain("matrix");
expect(written.rootHelpText).toContain("Usage:");
expect(written.rootHelpText).toContain("openclaw");
});
});