fix(ci): stabilize full release validation

This commit is contained in:
Peter Steinberger
2026-04-28 19:57:15 +01:00
parent f641691910
commit 4a24b23e3e
15 changed files with 144 additions and 64 deletions

View File

@@ -82,7 +82,7 @@ permissions:
concurrency:
group: full-release-validation-${{ inputs.ref }}
cancel-in-progress: false
cancel-in-progress: ${{ inputs.ref == 'main' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -158,7 +158,7 @@ permissions: read-all
concurrency:
group: openclaw-cross-os-release-checks-${{ inputs.ref }}-${{ inputs.provider }}-${{ inputs.mode }}
cancel-in-progress: false
cancel-in-progress: ${{ inputs.ref == 'main' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -333,6 +333,9 @@ jobs:
cache: pnpm
cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
- name: Ensure pnpm store cache directory exists
run: mkdir -p "$(pnpm store path --silent)"
- name: Build candidate artifact once
if: inputs.candidate_artifact_name == ''
env:

View File

@@ -56,7 +56,7 @@ on:
concurrency:
group: openclaw-release-checks-${{ inputs.ref }}
cancel-in-progress: false
cancel-in-progress: ${{ inputs.ref == 'main' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const runExec = vi.hoisted(() => vi.fn());
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => "/tmp/openclaw"));
const OPENCLAW_TMP_ROOT = "/tmp/openclaw";
const TRASH_SOURCE = `${OPENCLAW_TMP_ROOT}/demo`;
vi.mock("../process/exec.js", () => ({
runExec,
@@ -46,7 +48,7 @@ describe("browser trash", () => {
const cpSync = vi.spyOn(fs, "cpSync");
const rmSync = vi.spyOn(fs, "rmSync");
await expect(movePathToTrash("/tmp/openclaw/demo")).resolves.toBe(
await expect(movePathToTrash(TRASH_SOURCE)).resolves.toBe(
"/home/test/.Trash/demo-123-secure/demo",
);
expect(runExec).not.toHaveBeenCalled();
@@ -55,10 +57,7 @@ describe("browser trash", () => {
mode: 0o700,
});
expect(mkdtempSync).toHaveBeenCalledWith("/home/test/.Trash/demo-123-");
expect(renameSync).toHaveBeenCalledWith(
"/tmp/openclaw/demo",
"/home/test/.Trash/demo-123-secure/demo",
);
expect(renameSync).toHaveBeenCalledWith(TRASH_SOURCE, "/home/test/.Trash/demo-123-secure/demo");
expect(cpSync).not.toHaveBeenCalled();
expect(rmSync).not.toHaveBeenCalled();
});
@@ -79,12 +78,12 @@ describe("browser trash", () => {
const mkdtempSync = mockTrashContainer("secure");
const renameSync = vi.spyOn(fs, "renameSync").mockImplementation(() => undefined);
await expect(movePathToTrash("/tmp/openclaw/demo")).resolves.toBe(
await expect(movePathToTrash(TRASH_SOURCE)).resolves.toBe(
"/real/home/test/.Trash/demo-123-secure/demo",
);
expect(mkdtempSync).toHaveBeenCalledWith("/real/home/test/.Trash/demo-123-");
expect(renameSync).toHaveBeenCalledWith(
"/tmp/openclaw/demo",
TRASH_SOURCE,
"/real/home/test/.Trash/demo-123-secure/demo",
);
});
@@ -106,12 +105,15 @@ describe("browser trash", () => {
it("refuses to use a symlinked trash directory", async () => {
const { movePathToTrash } = await import("./trash.js");
vi.spyOn(fs, "mkdirSync").mockImplementation(() => undefined);
vi.spyOn(fs, "lstatSync").mockReturnValue({
isDirectory: () => true,
isSymbolicLink: () => true,
} as fs.Stats);
vi.spyOn(fs, "lstatSync").mockImplementation(
(candidate) =>
({
isDirectory: () => true,
isSymbolicLink: () => String(candidate) === "/home/test/.Trash",
}) as fs.Stats,
);
await expect(movePathToTrash("/tmp/openclaw/demo")).rejects.toThrow(
await expect(movePathToTrash(TRASH_SOURCE)).rejects.toThrow(
"Refusing to use non-directory/symlink trash directory",
);
});
@@ -127,22 +129,15 @@ describe("browser trash", () => {
const cpSync = vi.spyOn(fs, "cpSync").mockImplementation(() => undefined);
const rmSync = vi.spyOn(fs, "rmSync").mockImplementation(() => undefined);
await expect(movePathToTrash("/tmp/openclaw/demo")).resolves.toBe(
await expect(movePathToTrash(TRASH_SOURCE)).resolves.toBe(
"/home/test/.Trash/demo-123-secure/demo",
);
expect(cpSync).toHaveBeenCalledWith(
"/tmp/openclaw/demo",
"/home/test/.Trash/demo-123-secure/demo",
{
recursive: true,
force: false,
errorOnExist: true,
},
);
expect(rmSync).toHaveBeenCalledWith("/tmp/openclaw/demo", {
expect(cpSync).toHaveBeenCalledWith(TRASH_SOURCE, "/home/test/.Trash/demo-123-secure/demo", {
recursive: true,
force: false,
errorOnExist: true,
});
expect(rmSync).toHaveBeenCalledWith(TRASH_SOURCE, { recursive: true, force: false });
});
it("retries copy fallback when the copy destination is created concurrently", async () => {
@@ -164,12 +159,12 @@ describe("browser trash", () => {
.mockImplementation(() => undefined);
const rmSync = vi.spyOn(fs, "rmSync").mockImplementation(() => undefined);
await expect(movePathToTrash("/tmp/openclaw/demo")).resolves.toBe(
await expect(movePathToTrash(TRASH_SOURCE)).resolves.toBe(
"/home/test/.Trash/demo-123-second/demo",
);
expect(cpSync).toHaveBeenNthCalledWith(
1,
"/tmp/openclaw/demo",
TRASH_SOURCE,
"/home/test/.Trash/demo-123-first/demo",
{
recursive: true,
@@ -179,7 +174,7 @@ describe("browser trash", () => {
);
expect(cpSync).toHaveBeenNthCalledWith(
2,
"/tmp/openclaw/demo",
TRASH_SOURCE,
"/home/test/.Trash/demo-123-second/demo",
{
recursive: true,
@@ -203,17 +198,17 @@ describe("browser trash", () => {
})
.mockImplementation(() => undefined);
await expect(movePathToTrash("/tmp/openclaw/demo")).resolves.toBe(
await expect(movePathToTrash(TRASH_SOURCE)).resolves.toBe(
"/home/test/.Trash/demo-123-second/demo",
);
expect(renameSync).toHaveBeenNthCalledWith(
1,
"/tmp/openclaw/demo",
TRASH_SOURCE,
"/home/test/.Trash/demo-123-first/demo",
);
expect(renameSync).toHaveBeenNthCalledWith(
2,
"/tmp/openclaw/demo",
TRASH_SOURCE,
"/home/test/.Trash/demo-123-second/demo",
);
expect(Date.now).toHaveBeenCalledTimes(1);

View File

@@ -4,6 +4,12 @@
"private": true,
"description": "OpenClaw QA synthetic channel plugin",
"type": "module",
"exports": {
".": "./index.ts",
"./api.js": "./api.ts",
"./runtime-api.js": "./runtime-api.ts",
"./test-api.js": "./test-api.ts"
},
"dependencies": {
"typebox": "1.1.33"
},

View File

@@ -51,8 +51,12 @@ describe("startQaGatewayRpcClient", () => {
},
{ prompt: "hi" },
{
clientName: "gateway-client",
deviceIdentity: null,
expectFinal: true,
mode: "backend",
progress: false,
scopes: ["operator.admin"],
},
);
@@ -124,8 +128,12 @@ describe("startQaGatewayRpcClient", () => {
},
{},
{
clientName: "gateway-client",
deviceIdentity: null,
expectFinal: undefined,
mode: "backend",
progress: false,
scopes: ["operator.admin"],
},
);

View File

@@ -55,8 +55,12 @@ export async function startQaGatewayRpcClient(params: {
},
rpcParams ?? {},
{
clientName: "gateway-client",
deviceIdentity: null,
expectFinal: opts?.expectFinal,
mode: "backend",
progress: false,
scopes: ["operator.admin"],
},
),
);

View File

@@ -3,11 +3,20 @@ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../gateway/protocol/
import type { GatewayRpcOpts } from "./gateway-rpc.types.js";
import { withProgress } from "./progress.js";
type CallGatewayFromCliRuntimeExtra = {
clientName?: Parameters<typeof callGateway>[0]["clientName"];
mode?: Parameters<typeof callGateway>[0]["mode"];
deviceIdentity?: Parameters<typeof callGateway>[0]["deviceIdentity"];
expectFinal?: boolean;
progress?: boolean;
scopes?: Parameters<typeof callGateway>[0]["scopes"];
};
export async function callGatewayFromCliRuntime(
method: string,
opts: GatewayRpcOpts,
params?: unknown,
extra?: { expectFinal?: boolean; progress?: boolean },
extra?: CallGatewayFromCliRuntimeExtra,
) {
const showProgress = extra?.progress ?? opts.json !== true;
return await withProgress(
@@ -22,10 +31,12 @@ export async function callGatewayFromCliRuntime(
token: opts.token,
method,
params,
deviceIdentity: extra?.deviceIdentity,
expectFinal: extra?.expectFinal ?? Boolean(opts.expectFinal),
scopes: extra?.scopes,
timeoutMs: Number(opts.timeout ?? 10_000),
clientName: GATEWAY_CLIENT_NAMES.CLI,
mode: GATEWAY_CLIENT_MODES.CLI,
clientName: extra?.clientName ?? GATEWAY_CLIENT_NAMES.CLI,
mode: extra?.mode ?? GATEWAY_CLIENT_MODES.CLI,
}),
);
}

View File

@@ -1,6 +1,9 @@
import type { Command } from "commander";
export type { GatewayRpcOpts } from "./gateway-rpc.types.js";
import type { OperatorScope } from "../gateway/operator-scopes.js";
import type { GatewayClientMode, GatewayClientName } from "../gateway/protocol/client-info.js";
import type { DeviceIdentity } from "../infra/device-identity.js";
import type { GatewayRpcOpts } from "./gateway-rpc.types.js";
export type { GatewayRpcOpts } from "./gateway-rpc.types.js";
type GatewayRpcRuntimeModule = typeof import("./gateway-rpc.runtime.js");
@@ -23,7 +26,14 @@ export async function callGatewayFromCli(
method: string,
opts: GatewayRpcOpts,
params?: unknown,
extra?: { expectFinal?: boolean; progress?: boolean },
extra?: {
clientName?: GatewayClientName;
mode?: GatewayClientMode;
deviceIdentity?: DeviceIdentity | null;
expectFinal?: boolean;
progress?: boolean;
scopes?: OperatorScope[];
},
) {
const runtime = await loadGatewayRpcRuntime();
return await runtime.callGatewayFromCliRuntime(method, opts, params, extra);

View File

@@ -38,7 +38,13 @@ const EMPTY_MANIFEST_REGISTRY: PluginManifestRegistry = {
diagnostics: [],
};
afterEach(() => {
async function setBundledPluginsDirFixture(dir: string | undefined): Promise<void> {
const { setBundledPluginsDirOverrideForTest } = await import("../plugins/bundled-dir.js");
setBundledPluginsDirOverrideForTest(dir);
}
afterEach(async () => {
await setBundledPluginsDirFixture(undefined);
vi.unstubAllEnvs();
vi.resetModules();
cleanupTrackedTempDirs(tempDirs);
@@ -52,10 +58,12 @@ describe("plugin auto-enable preferOver", () => {
writeBundledChannelPackage(rootDir, channelId);
vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", rootDir);
await setBundledPluginsDirFixture(rootDir);
const { normalizeChatChannelId } = await import("../channels/ids.js");
expect(normalizeChatChannelId(channelId)).toBe(channelId);
vi.stubEnv("OPENCLAW_BUNDLED_PLUGINS_DIR", path.join(rootDir, "missing"));
await setBundledPluginsDirFixture(undefined);
const { materializePluginAutoEnableCandidates } = await import("./plugin-auto-enable.js");
const result = materializePluginAutoEnableCandidates({

View File

@@ -359,6 +359,15 @@ describe("handshake auth helpers", () => {
authMethod: "token",
}),
).toBe(true);
expect(
shouldSkipLocalBackendSelfPairing({
connectParams,
locality: "shared_secret_loopback_local",
hasBrowserOriginHeader: false,
sharedAuthOk: true,
authMethod: "token",
}),
).toBe(true);
expect(
shouldSkipLocalBackendSelfPairing({
connectParams,

View File

@@ -265,7 +265,7 @@ export function shouldSkipLocalBackendSelfPairing(params: {
const usesSharedSecretAuth = params.authMethod === "token" || params.authMethod === "password";
const usesDeviceTokenAuth = params.authMethod === "device-token";
return (
params.locality === "direct_local" &&
(params.locality === "direct_local" || params.locality === "shared_secret_loopback_local") &&
!params.hasBrowserOriginHeader &&
((params.sharedAuthOk && usesSharedSecretAuth) || usesDeviceTokenAuth)
);

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js";
import {
clearBundledRuntimeDependencyNodePaths,
resolveBundledRuntimeDependencyInstallRoot,
@@ -73,6 +74,11 @@ function createBundledPluginDir(prefix: string, marker: string): string {
return rootDir;
}
function useBundledPluginDirOverrideForTest(dir: string): void {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
setBundledPluginsDirOverrideForTest(dir);
}
function createThrowingPluginDir(prefix: string): string {
const rootDir = createTrustedBundledFixtureRoot(prefix);
const pluginDir = path.join(rootDir, "bad");
@@ -206,6 +212,7 @@ afterEach(() => {
}
resetFacadeLoaderStateForTest();
setFacadeLoaderJitiFactoryForTest(undefined);
setBundledPluginsDirOverrideForTest(undefined);
clearBundledRuntimeDependencyNodePaths();
delete (globalThis as typeof globalThis & Record<string, unknown>)[FACADE_LOADER_GLOBAL];
if (originalBundledPluginsDir === undefined) {
@@ -230,14 +237,14 @@ describe("plugin-sdk facade loader", () => {
const overrideA = createBundledPluginDir("openclaw-facade-loader-a-", "override-a");
const overrideB = createBundledPluginDir("openclaw-facade-loader-b-", "override-b");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideA;
useBundledPluginDirOverrideForTest(overrideA);
const fromA = loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "api.js",
});
expect(fromA.marker).toBe("override-a");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideB;
useBundledPluginDirOverrideForTest(overrideB);
const fromB = loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "api.js",
@@ -246,7 +253,8 @@ describe("plugin-sdk facade loader", () => {
});
it("falls back to package source surfaces when an override dir lacks a bundled plugin", () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = createTempDirSync("openclaw-facade-loader-empty-");
const overrideDir = createTrustedBundledFixtureRoot("openclaw-facade-loader-empty-");
useBundledPluginDirOverrideForTest(overrideDir);
const loaded = loadBundledPluginPublicSurfaceModuleSync<{
closeTrackedBrowserTabsForSessions: unknown;
@@ -272,7 +280,7 @@ describe("plugin-sdk facade loader", () => {
it("shares loaded facade ids with facade-runtime", () => {
const dir = createBundledPluginDir("openclaw-facade-loader-ids-", "identity-check");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
const first = loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "demo",
@@ -301,7 +309,7 @@ describe("plugin-sdk facade loader", () => {
'export const marker = "windows-dist-ok";\n',
"utf8",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
useBundledPluginDirOverrideForTest(bundledPluginsDir);
const createJitiCalls: Parameters<FacadeLoaderJitiFactory>[] = [];
setFacadeLoaderJitiFactoryForTest(((...args) => {
@@ -338,7 +346,7 @@ describe("plugin-sdk facade loader", () => {
marker: "staged",
prefix: "openclaw-facade-loader-runtime-deps-",
});
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
useBundledPluginDirOverrideForTest(fixture.bundledPluginsDir);
process.env.OPENCLAW_PLUGIN_STAGE_DIR = fixture.stageRoot;
await expect(import(pathToFileURL(fixture.modulePath).href)).rejects.toMatchObject({
@@ -367,6 +375,7 @@ describe("plugin-sdk facade loader", () => {
marker: "staged",
prefix: "openclaw-facade-loader-built-async-",
});
setBundledPluginsDirOverrideForTest(fixture.bundledPluginsDir);
const loaded = await loadBundledPluginPublicSurfaceModule<{
marker: string;
@@ -387,7 +396,7 @@ describe("plugin-sdk facade loader", () => {
it("breaks circular facade re-entry during module evaluation", () => {
const dir = createCircularPluginDir("openclaw-facade-loader-circular-");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
(globalThis as typeof globalThis & Record<string, unknown>)[FACADE_LOADER_GLOBAL] =
loadBundledPluginPublicSurfaceModuleSync;
@@ -401,7 +410,7 @@ describe("plugin-sdk facade loader", () => {
it("clears the cache on load failure so retries re-execute", () => {
const dir = createThrowingPluginDir("openclaw-facade-loader-throw-");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
expect(() =>
loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js";
import { createPluginActivationSource, normalizePluginsConfig } from "../plugins/config-state.js";
import { clearPluginDiscoveryCache } from "../plugins/discovery.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
@@ -59,6 +60,11 @@ function createBundledPluginDir(prefix: string, marker: string): string {
return rootDir;
}
function useBundledPluginDirOverrideForTest(dir: string): void {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
setBundledPluginsDirOverrideForTest(dir);
}
function createThrowingPluginDir(prefix: string): string {
const rootDir = createTrustedBundledFixtureRoot(prefix);
const pluginDir = path.join(rootDir, "bad");
@@ -86,6 +92,7 @@ afterEach(() => {
clearRuntimeConfigSnapshot();
resetFacadeRuntimeStateForTest();
resetFacadeActivationCheckRuntimeStateForTest();
setBundledPluginsDirOverrideForTest(undefined);
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
vi.doUnmock("../plugins/manifest-registry.js");
@@ -111,7 +118,7 @@ describe("plugin-sdk facade runtime", () => {
const overrideA = createBundledPluginDir("openclaw-facade-runtime-a-", "override-a");
const overrideB = createBundledPluginDir("openclaw-facade-runtime-b-", "override-b");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideA;
useBundledPluginDirOverrideForTest(overrideA);
const fromA = __testing.resolveFacadeModuleLocation({
dirName: "demo",
artifactBasename: "api.js",
@@ -121,7 +128,7 @@ describe("plugin-sdk facade runtime", () => {
boundaryRoot: overrideA,
});
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = overrideB;
useBundledPluginDirOverrideForTest(overrideB);
const fromB = __testing.resolveFacadeModuleLocation({
dirName: "demo",
artifactBasename: "api.js",
@@ -133,20 +140,18 @@ describe("plugin-sdk facade runtime", () => {
});
it("falls back to package source surfaces when an override dir is partial", () => {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = createBundledPluginDir(
"openclaw-facade-runtime-partial-",
"partial",
);
const overrideDir = createTrustedBundledFixtureRoot("openclaw-facade-runtime-empty-");
useBundledPluginDirOverrideForTest(overrideDir);
expect(
__testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
}),
).toEqual({
modulePath: path.resolve("extensions/browser/browser-maintenance.ts"),
boundaryRoot: path.resolve("."),
const resolved = __testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
});
expect(resolved?.boundaryRoot).not.toBe(overrideDir);
expect(resolved?.modulePath).toMatch(
/(?:^|\/)(?:extensions|dist-runtime\/extensions)\/browser\/browser-maintenance\.(?:ts|js)$/u,
);
});
it("does not fall back to package source surfaces when bundled plugins are disabled", () => {
@@ -163,7 +168,7 @@ describe("plugin-sdk facade runtime", () => {
it("returns the same object identity on repeated calls (sentinel consistency)", () => {
const dir = createBundledPluginDir("openclaw-facade-identity-", "identity-check");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
const location = {
modulePath: path.join(dir, "demo", "api.js"),
boundaryRoot: dir,
@@ -245,7 +250,7 @@ describe("plugin-sdk facade runtime", () => {
});
it("clears the cache on load failure so retries re-execute", () => {
const dir = createThrowingPluginDir("openclaw-facade-throw-");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
expect(() =>
loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
@@ -479,7 +484,7 @@ describe("plugin-sdk facade runtime", () => {
}),
"utf8",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
useBundledPluginDirOverrideForTest(dir);
setRuntimeConfigSnapshot(
{
plugins: {},

View File

@@ -7,6 +7,7 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { resolveUserPath } from "../utils.js";
const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bundled-plugins");
let bundledPluginsDirOverrideForTest: string | undefined;
export function areBundledPluginsDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
const raw = normalizeOptionalLowercaseString(env.OPENCLAW_DISABLE_BUNDLED_PLUGINS);
@@ -173,6 +174,10 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
return resolveDisabledBundledPluginsDir();
}
if (bundledPluginsDirOverrideForTest) {
return bundledPluginsDirOverrideForTest;
}
const override = env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim();
let rejectedExistingOverride: string | null = null;
if (override) {
@@ -248,3 +253,10 @@ export function resolveBundledPluginsDir(env: NodeJS.ProcessEnv = process.env):
return undefined;
}
export function setBundledPluginsDirOverrideForTest(dir: string | undefined): void {
if (process.env.VITEST !== "true" && process.env.NODE_ENV !== "test") {
throw new Error("setBundledPluginsDirOverrideForTest is only available in tests");
}
bundledPluginsDirOverrideForTest = dir;
}