fix: restore ci gates on main

This commit is contained in:
Peter Steinberger
2026-04-28 19:42:50 +01:00
parent bb0461b682
commit f2f34e5f35
11 changed files with 136 additions and 35 deletions

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import {
@@ -15,20 +16,32 @@ import {
} from "./test-helpers/temp-plugin-extension-fixtures.js";
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
const tempDirs: string[] = [];
function createTempDir(): string {
return createTempPluginDir(tempDirs, "openclaw-codex-ext-");
}
function createBundledTempDir(): string {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
return createTempPluginDir(tempDirs, "openclaw-codex-ext-", {
parentDir: path.join(process.cwd(), "dist-runtime", "extensions"),
});
}
afterEach(() => {
clearRuntimeConfigSnapshot();
cleanupTempPluginTestEnvironment(tempDirs, originalBundledPluginsDir);
cleanupTempPluginTestEnvironment(
tempDirs,
originalBundledPluginsDir,
originalDisableBundledPlugins,
);
});
describe("agent tool result middleware", () => {
it("includes plugin-registered middleware and restores it from cache", async () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -81,7 +94,7 @@ describe("agent tool result middleware", () => {
});
it("rejects middleware when the manifest omits the runtime contract", () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -158,7 +171,7 @@ describe("agent tool result middleware", () => {
});
it("merges runtimes when a plugin registers the same middleware function twice", () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -194,7 +207,7 @@ export default { id: "tool-result-middleware", register(api) {
});
it("lazily loads bundled middleware owners from manifest contracts", async () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -246,7 +259,7 @@ export default { id: "tool-result-middleware", register(api) {
describe("Codex app-server extension factories", () => {
it("includes plugin-registered Codex app-server extension factories and restores them from cache", async () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -337,7 +350,7 @@ describe("Codex app-server extension factories", () => {
});
it("rejects bundled plugins that omit the Codex app-server extension contract", () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({
@@ -373,7 +386,7 @@ describe("Codex app-server extension factories", () => {
});
it("rejects non-function Codex app-server extension factories from bundled plugins", () => {
const tmp = createTempDir();
const tmp = createBundledTempDir();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tmp;
writeTempPlugin({

View File

@@ -7,8 +7,14 @@ import { setActivePluginRegistry } from "../../plugins/runtime.js";
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
export function createTempPluginDir(tempDirs: string[], prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
export function createTempPluginDir(
tempDirs: string[],
prefix: string,
options?: { parentDir?: string },
): string {
const parentDir = options?.parentDir ?? os.tmpdir();
fs.mkdirSync(parentDir, { recursive: true });
const dir = fs.mkdtempSync(path.join(parentDir, prefix));
tempDirs.push(dir);
return dir;
}
@@ -43,6 +49,7 @@ export function writeTempPlugin(params: {
export function cleanupTempPluginTestEnvironment(
tempDirs: string[],
originalBundledPluginsDir: string | undefined,
originalDisableBundledPlugins?: string,
) {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
@@ -54,6 +61,11 @@ export function cleanupTempPluginTestEnvironment(
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalDisableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
}
}
export function resetActivePluginRegistryForTest() {

View File

@@ -15,6 +15,20 @@ function makeTempDir() {
return makeTrackedTempDir("openclaw-doctor-plugin-manifests", tempDirs);
}
function makePluginWorkspace() {
const workspaceDir = makeTempDir();
return {
workspaceDir,
pluginsRoot: path.join(workspaceDir, ".openclaw", "extensions"),
env: {
...process.env,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
},
};
}
function writeManifest(dir: string, manifest: Record<string, unknown>) {
fs.writeFileSync(
path.join(dir, "openclaw.plugin.json"),
@@ -76,7 +90,7 @@ describe("doctor plugin manifest legacy contract repair", () => {
});
it("collects legacy top-level capability keys for migration", () => {
const pluginsRoot = makeTempDir();
const { pluginsRoot } = makePluginWorkspace();
const root = path.join(pluginsRoot, "openai");
fs.mkdirSync(root, { recursive: true });
writePackageJson(root);
@@ -101,7 +115,7 @@ describe("doctor plugin manifest legacy contract repair", () => {
});
it("rewrites legacy top-level capability keys into contracts", async () => {
const pluginsRoot = makeTempDir();
const { pluginsRoot } = makePluginWorkspace();
const root = path.join(pluginsRoot, "openai");
fs.mkdirSync(root, { recursive: true });
writePackageJson(root);
@@ -141,7 +155,7 @@ describe("doctor plugin manifest legacy contract repair", () => {
});
it("ignores non-object contracts payloads when collecting migrations", () => {
const pluginsRoot = makeTempDir();
const { pluginsRoot } = makePluginWorkspace();
const root = path.join(pluginsRoot, "openai");
fs.mkdirSync(root, { recursive: true });
writePackageJson(root);

View File

@@ -83,6 +83,7 @@ function buildLegacyManifestContractMigration(params: {
export function collectLegacyPluginManifestContractMigrations(params?: {
env?: NodeJS.ProcessEnv;
manifestRoots?: string[];
workspaceDir?: string;
}): LegacyManifestContractMigration[] {
const seen = new Set<string>();
const migrations: LegacyManifestContractMigration[] = [];
@@ -114,6 +115,7 @@ export function collectLegacyPluginManifestContractMigrations(params?: {
for (const plugin of loadPluginManifestRegistry({
cache: false,
...(params?.env ? { env: params.env } : {}),
...(params?.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
}).plugins) {
if (seen.has(plugin.manifestPath)) {
continue;
@@ -138,6 +140,7 @@ export function collectLegacyPluginManifestContractMigrations(params?: {
export async function maybeRepairLegacyPluginManifestContracts(params: {
env?: NodeJS.ProcessEnv;
manifestRoots?: string[];
workspaceDir?: string;
runtime: RuntimeEnv;
prompter: DoctorPrompter;
note?: typeof note;
@@ -145,6 +148,7 @@ export async function maybeRepairLegacyPluginManifestContracts(params: {
const migrations = collectLegacyPluginManifestContractMigrations({
...(params.env ? { env: params.env } : {}),
...(params.manifestRoots ? { manifestRoots: params.manifestRoots } : {}),
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
});
if (migrations.length === 0) {
return;

View File

@@ -35,6 +35,7 @@ const GATEWAY_TEST_ENV_KEYS = [
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
] as const;
function nextGatewayId(prefix: string): string {
@@ -110,6 +111,7 @@ async function setupGatewayTempHome(params: { prefix: string; minimalGateway?: b
const workspaceDir = path.join(tempHome, "openclaw");
await fs.mkdir(workspaceDir, { recursive: true });
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = await createEmptyBundledPluginsDir(tempHome);
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
return { envSnapshot, tempHome, workspaceDir };
}
@@ -318,6 +320,7 @@ module.exports = {
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
"OPENCLAW_TEST_MINIMAL_GATEWAY",
]);
@@ -333,6 +336,7 @@ module.exports = {
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-wizard-home-"));
process.env.HOME = tempHome;
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = await createEmptyBundledPluginsDir(tempHome);
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_STATE_DIR;
delete process.env.OPENCLAW_CONFIG_PATH;

View File

@@ -815,11 +815,18 @@ describe("gateway server misc", () => {
"utf-8",
);
await withEnvAsync({ OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, async () => {
const autoPort = await getFreePort();
const autoServer = await startGatewayServer(autoPort);
await autoServer.close();
});
await withEnvAsync(
{
OPENCLAW_TEST_MINIMAL_GATEWAY: undefined,
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.resolve("extensions"),
},
async () => {
const autoPort = await getFreePort();
const autoServer = await startGatewayServer(autoPort);
await autoServer.close();
},
);
const updated = JSON.parse(await fs.readFile(configPath, "utf-8")) as Record<string, unknown>;
const channels = updated.channels as Record<string, unknown> | undefined;

View File

@@ -70,6 +70,7 @@ const GATEWAY_TEST_ENV_KEYS = [
"OPENCLAW_SKIP_GMAIL_WATCHER",
"OPENCLAW_SKIP_CANVAS_HOST",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_SKIP_CRON",
@@ -235,6 +236,7 @@ function applyGatewaySkipEnv() {
process.env.OPENCLAW_SKIP_PROVIDERS = "1";
process.env.OPENCLAW_SKIP_CRON = "1";
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1";
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tempHome
? path.join(tempHome, "openclaw-test-no-bundled-extensions")
: "openclaw-test-no-bundled-extensions";

View File

@@ -203,6 +203,23 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
});
});
it("uses the Node-adjacent POSIX npm shim when npm-cli.js is unavailable", () => {
const execPath = "/opt/node/bin/node";
const npmPath = "/opt/node/bin/npm";
const runner = resolveBundledRuntimeDepsNpmRunner({
env: {},
execPath,
existsSync: (candidate) => candidate === npmPath,
npmArgs: ["install", "acpx@0.5.3"],
platform: "linux",
});
expect(runner).toEqual({
command: npmPath,
args: ["install", "acpx@0.5.3"],
});
});
it("refuses Windows shell fallback when no safe npm executable is available", () => {
expect(() =>
resolveBundledRuntimeDepsNpmRunner({
@@ -222,7 +239,7 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => {
PATH: "/repo/evil/bin:/usr/bin:/bin",
},
execPath: "/opt/node/bin/node",
existsSync: (candidate) => candidate === "/opt/node/bin/npm",
existsSync: (candidate) => candidate === "/usr/bin/npm",
npmArgs: ["install"],
platform: "linux",
}),

View File

@@ -1325,6 +1325,14 @@ export function resolveBundledRuntimeDepsNpmRunner(params: {
throw new Error("Unable to resolve a safe npm executable on Windows");
}
const npmExePath = pathImpl.resolve(nodeDir, "npm");
if (existsSync(npmExePath)) {
return {
command: npmExePath,
args: params.npmArgs,
};
}
throw new Error("Unable to resolve a safe npm executable");
}
type BundledPluginRuntimeDepsManifest = {

View File

@@ -45,6 +45,7 @@ function collectBundledChannelOwnerPluginIds(params: {
config: OpenClawConfig;
channelIds: readonly string[];
env: NodeJS.ProcessEnv;
bundledPluginsDir?: string;
}): string[] {
const plugins = normalizePluginsConfig(params.config.plugins);
const channelIds = new Set(
@@ -55,7 +56,7 @@ function collectBundledChannelOwnerPluginIds(params: {
if (channelIds.size === 0) {
return [];
}
const bundledDir = resolveBundledPluginsDir(params.env);
const bundledDir = params.bundledPluginsDir ?? resolveBundledPluginsDir(params.env);
if (!bundledDir) {
return [];
}
@@ -126,6 +127,7 @@ export function resolveEffectivePluginIds(params: {
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
bundledPluginsDir?: string;
}): string[] {
const autoEnabled = applyPluginAutoEnable({
config: params.config,
@@ -150,6 +152,7 @@ export function resolveEffectivePluginIds(params: {
config: effectiveConfig,
channelIds: configuredChannelIds,
env: params.env,
...(params.bundledPluginsDir ? { bundledPluginsDir: params.bundledPluginsDir } : {}),
})) {
ids.add(pluginId);
}