mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 00:50:42 +00:00
Every CLI invocation reads the config snapshot, which pulls bundled channel doctor contracts and setup surfaces through `getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds several seconds of per-load overhead on slower hosts (NAS profiling shows ~78% of `openclaw config get` wall time spent inside the jiti library), and that overhead is pure waste for the already-compiled `.js` artifacts shipped in dist/. Wrap the loader returned by `getCachedPluginJitiLoader` so that compiled JS targets go through `tryNativeRequireJavaScriptModule` first. Jiti stays on the hot path for: - TS/TSX/MTS/CTS sources - paths the native-require helper declines (Windows by default, or module-resolution fallbacks) This centralises the fast path that already existed — inside `doctor-contract-registry` and `channel-entry-contract` — and extends it to every caller that goes through the jiti loader cache. Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord configured): | command | before | after | |------------------|-------:|------:| | config get X | 24s | 6s | | status | 45s | 18s | | devices list | 55s | 26s | | nodes status | 55s | 26s | Fixes the slow config/status/devices/nodes read paths reported in openclaw#62842. Remaining time is dominated by non-jiti code paths (config schema validation, eager provider-plugin module eval) that are out of scope for this patch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
833 lines
26 KiB
TypeScript
833 lines
26 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { shouldExpectNativeJitiForJavaScriptTestRuntime } from "../test-utils/jiti-runtime.js";
|
|
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
|
import {
|
|
getRegistryJitiMocks,
|
|
resetRegistryJitiMocks,
|
|
} from "./test-helpers/registry-jiti-mocks.js";
|
|
|
|
// jiti-loader-cache prefers native require() for compiled .js before falling
|
|
// back to jiti. These tests scripts plugin-loading behaviour through the
|
|
// jiti mock — disable the native-require fast path so the mocked jiti loader
|
|
// stays authoritative for the test fixture files on disk.
|
|
vi.mock("./native-module-require.js", () => ({
|
|
isJavaScriptModulePath: (_modulePath: string) => false,
|
|
tryNativeRequireJavaScriptModule: (_modulePath: string) => ({ ok: false }),
|
|
}));
|
|
|
|
const tempDirs: string[] = [];
|
|
const mocks = getRegistryJitiMocks();
|
|
|
|
let clearPluginSetupRegistryCache: typeof import("./setup-registry.js").clearPluginSetupRegistryCache;
|
|
let setupRegistryTesting: typeof import("./setup-registry.js").__testing;
|
|
let resolvePluginSetupRegistry: typeof import("./setup-registry.js").resolvePluginSetupRegistry;
|
|
let resolvePluginSetupProvider: typeof import("./setup-registry.js").resolvePluginSetupProvider;
|
|
let resolvePluginSetupCliBackend: typeof import("./setup-registry.js").resolvePluginSetupCliBackend;
|
|
let runPluginSetupConfigMigrations: typeof import("./setup-registry.js").runPluginSetupConfigMigrations;
|
|
|
|
function forceNodeRuntimeVersionsForTest(): () => void {
|
|
const originalVersions = process.versions;
|
|
const nodeVersions = { ...originalVersions } as NodeJS.ProcessVersions & {
|
|
bun?: string | undefined;
|
|
};
|
|
delete nodeVersions.bun;
|
|
Object.defineProperty(process, "versions", {
|
|
configurable: true,
|
|
value: nodeVersions,
|
|
});
|
|
return () => {
|
|
Object.defineProperty(process, "versions", {
|
|
configurable: true,
|
|
value: originalVersions,
|
|
});
|
|
};
|
|
}
|
|
|
|
function makeTempDir(): string {
|
|
return makeTrackedTempDir("openclaw-setup-registry", tempDirs);
|
|
}
|
|
|
|
function writeSetupApiStub(pluginRoot: string): void {
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
}
|
|
|
|
function mockSinglePlugin(plugin: {
|
|
id: string;
|
|
rootDir: string;
|
|
setup?: unknown;
|
|
configContracts?: unknown;
|
|
}) {
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [plugin],
|
|
diagnostics: [],
|
|
});
|
|
}
|
|
|
|
function mockVoiceCallConfigMigrationRegistration(registerResult?: () => Promise<void>) {
|
|
const pluginRoot = makeTempDir();
|
|
writeSetupApiStub(pluginRoot);
|
|
mockSinglePlugin({ id: "voice-call", rootDir: pluginRoot });
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
|
|
}) {
|
|
api.registerConfigMigration((config) => ({ config, changes: ["voice-call"] }));
|
|
return registerResult?.();
|
|
},
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function mockOpenAiCliBackendRegistration(params: {
|
|
requiresRuntime?: boolean;
|
|
registerResult?: () => Promise<void>;
|
|
}) {
|
|
const pluginRoot = makeTempDir();
|
|
writeSetupApiStub(pluginRoot);
|
|
mockSinglePlugin({
|
|
id: "openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
cliBackends: ["codex-cli"],
|
|
...(params.requiresRuntime ? { requiresRuntime: true } : {}),
|
|
},
|
|
});
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
|
|
}) {
|
|
api.registerCliBackend({
|
|
id: "codex-cli",
|
|
config: { command: "codex" },
|
|
});
|
|
return params.registerResult?.();
|
|
},
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function mockDuplicateSetupClaims(params: {
|
|
duplicatePluginId: boolean;
|
|
kind: "cliBackend" | "provider";
|
|
}) {
|
|
const bundledRoot = makeTempDir();
|
|
const workspaceRoot = makeTempDir();
|
|
writeSetupApiStub(bundledRoot);
|
|
writeSetupApiStub(workspaceRoot);
|
|
const setup =
|
|
params.kind === "provider"
|
|
? {
|
|
bundled: { providers: [{ id: "openai" }] },
|
|
workspace: { providers: [{ id: "OpenAI" }] },
|
|
}
|
|
: {
|
|
bundled: { cliBackends: ["codex-cli"] },
|
|
workspace: { cliBackends: ["CODEX-CLI"] },
|
|
};
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
origin: "bundled",
|
|
rootDir: bundledRoot,
|
|
setup: setup.bundled,
|
|
},
|
|
{
|
|
id: params.duplicatePluginId ? "openai" : "workspace-shadow",
|
|
origin: "workspace",
|
|
rootDir: workspaceRoot,
|
|
setup: setup.workspace,
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
}
|
|
|
|
async function expectNoUnhandledRejection(run: () => void | Promise<void>): Promise<void> {
|
|
const unhandledRejections: unknown[] = [];
|
|
const onUnhandledRejection = (reason: unknown) => {
|
|
unhandledRejections.push(reason);
|
|
};
|
|
process.on("unhandledRejection", onUnhandledRejection);
|
|
try {
|
|
await run();
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
} finally {
|
|
process.off("unhandledRejection", onUnhandledRejection);
|
|
}
|
|
expect(unhandledRejections).toEqual([]);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanupTrackedTempDirs(tempDirs);
|
|
});
|
|
|
|
describe("setup-registry getJiti", () => {
|
|
beforeEach(async () => {
|
|
resetRegistryJitiMocks();
|
|
vi.resetModules();
|
|
({
|
|
__testing: setupRegistryTesting,
|
|
clearPluginSetupRegistryCache,
|
|
resolvePluginSetupRegistry,
|
|
resolvePluginSetupProvider,
|
|
resolvePluginSetupCliBackend,
|
|
runPluginSetupConfigMigrations,
|
|
} = await import("./setup-registry.js"));
|
|
clearPluginSetupRegistryCache();
|
|
});
|
|
|
|
it("uses the runtime-supported Jiti boundary on Windows for setup-api modules", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [{ id: "test-plugin", rootDir: pluginRoot }],
|
|
diagnostics: [],
|
|
});
|
|
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
|
const restoreVersions = forceNodeRuntimeVersionsForTest();
|
|
const expectedTryNative = shouldExpectNativeJitiForJavaScriptTestRuntime();
|
|
|
|
try {
|
|
resolvePluginSetupRegistry({
|
|
workspaceDir: pluginRoot,
|
|
env: {},
|
|
});
|
|
} finally {
|
|
restoreVersions();
|
|
platformSpy.mockRestore();
|
|
}
|
|
|
|
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
|
|
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
|
|
expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
|
|
expect.objectContaining({
|
|
tryNative: expectedTryNative,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("passes explicit plugin id scope into setup manifest reads", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [{ id: "test-plugin", rootDir: pluginRoot }],
|
|
diagnostics: [],
|
|
});
|
|
|
|
resolvePluginSetupRegistry({
|
|
pluginIds: ["test-plugin"],
|
|
env: {},
|
|
});
|
|
|
|
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
pluginIds: ["test-plugin"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("skips setup-api loading when config has no relevant migration triggers", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "amazon-bedrock",
|
|
rootDir: pluginRoot,
|
|
configContracts: {
|
|
compatibilityMigrationPaths: ["models.bedrockDiscovery"],
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
|
|
}) {
|
|
api.registerConfigMigration((config) => ({ config, changes: ["unexpected"] }));
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
const result = runPluginSetupConfigMigrations({
|
|
config: {
|
|
models: {
|
|
providers: {
|
|
openai: { baseUrl: "https://api.openai.com/v1" },
|
|
},
|
|
},
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
expect(result.changes).toEqual([]);
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("loads only plugins whose manifest migration triggers match the config", () => {
|
|
const bedrockRoot = makeTempDir();
|
|
const voiceCallRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(bedrockRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
fs.writeFileSync(path.join(voiceCallRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "amazon-bedrock",
|
|
rootDir: bedrockRoot,
|
|
configContracts: {
|
|
compatibilityMigrationPaths: ["models.bedrockDiscovery"],
|
|
},
|
|
},
|
|
{
|
|
id: "voice-call",
|
|
rootDir: voiceCallRoot,
|
|
configContracts: {
|
|
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation((modulePath: string) => {
|
|
const pluginId = modulePath.includes(bedrockRoot) ? "amazon-bedrock" : "voice-call";
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
|
|
}) {
|
|
api.registerConfigMigration((config) => ({
|
|
config,
|
|
changes: [pluginId],
|
|
}));
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
const result = runPluginSetupConfigMigrations({
|
|
config: {
|
|
models: {
|
|
bedrockDiscovery: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
expect(result.changes).toEqual(["amazon-bedrock"]);
|
|
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
|
|
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(bedrockRoot, "setup-api.js"));
|
|
});
|
|
|
|
it("still loads explicitly configured plugin entries without manifest trigger metadata", () => {
|
|
mockVoiceCallConfigMigrationRegistration();
|
|
|
|
const result = runPluginSetupConfigMigrations({
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
"voice-call": {
|
|
config: {
|
|
provider: "log",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
env: {},
|
|
});
|
|
|
|
expect(result.changes).toEqual(["voice-call"]);
|
|
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("prefers setup provider descriptors over top-level provider ids", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "amazon-bedrock",
|
|
rootDir: pluginRoot,
|
|
providers: ["legacy-bedrock"],
|
|
setup: {
|
|
providers: [{ id: "amazon-bedrock" }],
|
|
requiresRuntime: true,
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
|
|
}) {
|
|
api.registerProvider({
|
|
id: "amazon-bedrock",
|
|
label: "Amazon Bedrock",
|
|
auth: [],
|
|
});
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(resolvePluginSetupProvider({ provider: "amazon-bedrock", env: {} })).toEqual(
|
|
expect.objectContaining({
|
|
id: "amazon-bedrock",
|
|
label: "Amazon Bedrock",
|
|
}),
|
|
);
|
|
expect(resolvePluginSetupProvider({ provider: "legacy-bedrock", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
|
|
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
|
|
});
|
|
|
|
it("treats explicit descriptor-only setup as a runtime cutoff", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(
|
|
path.join(pluginRoot, "setup-api.js"),
|
|
"export default { register(api) { api.registerProvider({ id: 'openai', label: 'OpenAI', auth: [] }); api.registerCliBackend({ id: 'codex-cli', config: { command: 'codex' } }); } };\n",
|
|
"utf-8",
|
|
);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "openai" }],
|
|
cliBackends: ["codex-cli"],
|
|
requiresRuntime: false,
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
|
|
expect(resolvePluginSetupRegistry({ env: {} })).toEqual({
|
|
providers: [],
|
|
cliBackends: [],
|
|
configMigrations: [],
|
|
autoEnableProbes: [],
|
|
diagnostics: [
|
|
expect.objectContaining({
|
|
pluginId: "openai",
|
|
code: "setup-descriptor-runtime-disabled",
|
|
}),
|
|
],
|
|
});
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not report descriptor-only diagnostics for bundled setup-api fallback paths", () => {
|
|
const parentDir = makeTempDir();
|
|
const pluginRoot = path.join(parentDir, "openai");
|
|
fs.mkdirSync(pluginRoot);
|
|
expect(fs.existsSync(path.join(process.cwd(), "extensions", "openai", "setup-api.ts"))).toBe(
|
|
true,
|
|
);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "workspace-openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "workspace-openai" }],
|
|
requiresRuntime: false,
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
|
|
expect(resolvePluginSetupRegistry({ env: {} })).toEqual({
|
|
providers: [],
|
|
cliBackends: [],
|
|
configMigrations: [],
|
|
autoEnableProbes: [],
|
|
diagnostics: [],
|
|
});
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports setup descriptor drift without rejecting runtime registrations", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "openai" }],
|
|
cliBackends: ["codex-cli"],
|
|
requiresRuntime: true,
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
|
|
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
|
|
}) {
|
|
api.registerProvider({
|
|
id: "anthropic",
|
|
label: "Anthropic",
|
|
auth: [],
|
|
});
|
|
api.registerCliBackend({
|
|
id: "claude-cli",
|
|
config: { command: "claude" },
|
|
});
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
const registry = resolvePluginSetupRegistry({ env: {} });
|
|
|
|
expect(registry.providers.map((entry) => entry.provider.id)).toEqual(["anthropic"]);
|
|
expect(registry.cliBackends.map((entry) => entry.backend.id)).toEqual(["claude-cli"]);
|
|
expect(registry.diagnostics).toEqual([
|
|
expect.objectContaining({
|
|
pluginId: "openai",
|
|
code: "setup-descriptor-provider-missing-runtime",
|
|
declaredId: "openai",
|
|
}),
|
|
expect.objectContaining({
|
|
pluginId: "openai",
|
|
code: "setup-descriptor-provider-runtime-undeclared",
|
|
runtimeId: "anthropic",
|
|
}),
|
|
expect.objectContaining({
|
|
pluginId: "openai",
|
|
code: "setup-descriptor-cli-backend-missing-runtime",
|
|
declaredId: "codex-cli",
|
|
}),
|
|
expect.objectContaining({
|
|
pluginId: "openai",
|
|
code: "setup-descriptor-cli-backend-runtime-undeclared",
|
|
runtimeId: "claude-cli",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("does not report drift when setup descriptors match runtime registrations", () => {
|
|
mockOpenAiCliBackendRegistration({
|
|
requiresRuntime: true,
|
|
});
|
|
|
|
expect(resolvePluginSetupRegistry({ env: {} }).diagnostics).toEqual([]);
|
|
});
|
|
|
|
it("does not load setup-api modules from the current working directory", () => {
|
|
const pluginRoot = makeTempDir();
|
|
const workspaceRoot = makeTempDir();
|
|
// The old cwd-fallback derived the lookup subdirectory from
|
|
// `path.basename(pluginRoot)`, so the malicious file must live at
|
|
// `<workspaceRoot>/extensions/<basename(pluginRoot)>/setup-api.js` to
|
|
// actually reproduce the pre-fix behavior. Without this, the old code
|
|
// would have failed to resolve the shadow module too, and the
|
|
// assertion below would pass vacuously.
|
|
const shadowDirName = path.basename(pluginRoot);
|
|
const maliciousExtensionRoot = path.join(workspaceRoot, "extensions", shadowDirName);
|
|
fs.mkdirSync(maliciousExtensionRoot, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(maliciousExtensionRoot, "setup-api.js"),
|
|
"export default { register(api) { api.registerProvider({ id: 'openai', label: 'OpenAI', auth: [] }); } };\n",
|
|
"utf-8",
|
|
);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "workspace-shadow",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "openai" }],
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
|
|
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(workspaceRoot);
|
|
try {
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
|
|
} finally {
|
|
cwdSpy.mockRestore();
|
|
}
|
|
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves setup cli backends from descriptors without loading every setup-api", () => {
|
|
const openaiRoot = makeTempDir();
|
|
const anthropicRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(openaiRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
fs.writeFileSync(path.join(anthropicRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
rootDir: openaiRoot,
|
|
cliBackends: ["legacy-openai-cli"],
|
|
setup: {
|
|
cliBackends: ["codex-cli"],
|
|
requiresRuntime: true,
|
|
},
|
|
},
|
|
{
|
|
id: "anthropic",
|
|
rootDir: anthropicRoot,
|
|
cliBackends: ["claude-cli"],
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation((modulePath: string) => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
|
|
}) {
|
|
api.registerCliBackend(
|
|
modulePath.includes(openaiRoot)
|
|
? { id: "codex-cli", config: { command: "codex" } }
|
|
: { id: "claude-cli", config: { command: "claude" } },
|
|
);
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
const first = resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} });
|
|
const second = resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} });
|
|
|
|
expect(first).toEqual({
|
|
pluginId: "openai",
|
|
backend: {
|
|
id: "codex-cli",
|
|
config: {
|
|
command: "codex",
|
|
},
|
|
},
|
|
});
|
|
expect(second).toEqual(first);
|
|
expect(resolvePluginSetupCliBackend({ backend: "legacy-openai-cli", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
|
|
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(openaiRoot, "setup-api.js"));
|
|
});
|
|
|
|
it("keeps synchronously registered cli backends even when register returns a promise", () => {
|
|
mockOpenAiCliBackendRegistration({
|
|
requiresRuntime: true,
|
|
registerResult: () => Promise.resolve(),
|
|
});
|
|
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toEqual({
|
|
pluginId: "openai",
|
|
backend: {
|
|
id: "codex-cli",
|
|
config: {
|
|
command: "codex",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("swallows rejected async setup provider registration returns", async () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "openai" }],
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
mocks.createJiti.mockImplementation(() => {
|
|
return () => ({
|
|
default: {
|
|
register(api: {
|
|
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
|
|
}) {
|
|
api.registerProvider({
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
auth: [],
|
|
});
|
|
return Promise.reject(new Error("async provider register failed"));
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
await expectNoUnhandledRejection(() => {
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toEqual(
|
|
expect.objectContaining({
|
|
id: "openai",
|
|
label: "OpenAI",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("swallows rejected async setup cli backend registration returns", async () => {
|
|
mockOpenAiCliBackendRegistration({
|
|
registerResult: () => Promise.reject(new Error("async cli backend register failed")),
|
|
});
|
|
|
|
await expectNoUnhandledRejection(() => {
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toEqual({
|
|
pluginId: "openai",
|
|
backend: {
|
|
id: "codex-cli",
|
|
config: {
|
|
command: "codex",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
it("swallows rejected async setup registry registration returns", async () => {
|
|
mockVoiceCallConfigMigrationRegistration(() =>
|
|
Promise.reject(new Error("async setup registry register failed")),
|
|
);
|
|
|
|
await expectNoUnhandledRejection(() => {
|
|
expect(resolvePluginSetupRegistry({ env: {} }).configMigrations).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
it("fails closed when multiple plugins claim the same setup provider id", () => {
|
|
mockDuplicateSetupClaims({
|
|
duplicatePluginId: false,
|
|
kind: "provider",
|
|
});
|
|
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when duplicate plugin ids shadow the same setup provider id", () => {
|
|
mockDuplicateSetupClaims({
|
|
duplicatePluginId: true,
|
|
kind: "provider",
|
|
});
|
|
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when multiple plugins claim the same setup cli backend id", () => {
|
|
mockDuplicateSetupClaims({
|
|
duplicatePluginId: false,
|
|
kind: "cliBackend",
|
|
});
|
|
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when duplicate plugin ids shadow the same setup cli backend id", () => {
|
|
mockDuplicateSetupClaims({
|
|
duplicatePluginId: true,
|
|
kind: "cliBackend",
|
|
});
|
|
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
|
|
expect(mocks.createJiti).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("bounds setup lookup caches with least-recently-used eviction", () => {
|
|
const pluginRoot = makeTempDir();
|
|
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
|
|
setupRegistryTesting.setMaxSetupLookupCacheEntriesForTest(1);
|
|
mocks.loadPluginManifestRegistry.mockReturnValue({
|
|
plugins: [
|
|
{
|
|
id: "openai",
|
|
rootDir: pluginRoot,
|
|
setup: {
|
|
providers: [{ id: "openai" }, { id: "anthropic" }],
|
|
cliBackends: ["codex-cli", "claude-cli"],
|
|
requiresRuntime: true,
|
|
},
|
|
},
|
|
],
|
|
diagnostics: [],
|
|
});
|
|
const loadSetupModule = vi.fn(() => ({
|
|
default: {
|
|
register(api: {
|
|
registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
|
|
registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
|
|
}) {
|
|
api.registerProvider({ id: "openai", label: "OpenAI", auth: [] });
|
|
api.registerProvider({ id: "anthropic", label: "Anthropic", auth: [] });
|
|
api.registerCliBackend({ id: "codex-cli", config: { command: "codex" } });
|
|
api.registerCliBackend({ id: "claude-cli", config: { command: "claude" } });
|
|
},
|
|
},
|
|
}));
|
|
mocks.createJiti.mockImplementation(() => loadSetupModule);
|
|
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })?.id).toBe("openai");
|
|
expect(resolvePluginSetupProvider({ provider: "anthropic", env: {} })?.id).toBe("anthropic");
|
|
expect(setupRegistryTesting.getCacheSizes().setupProvider).toBe(1);
|
|
expect(resolvePluginSetupProvider({ provider: "openai", env: {} })?.id).toBe("openai");
|
|
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })?.backend.id).toBe(
|
|
"codex-cli",
|
|
);
|
|
expect(resolvePluginSetupCliBackend({ backend: "claude-cli", env: {} })?.backend.id).toBe(
|
|
"claude-cli",
|
|
);
|
|
expect(setupRegistryTesting.getCacheSizes().setupCliBackend).toBe(1);
|
|
expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })?.backend.id).toBe(
|
|
"codex-cli",
|
|
);
|
|
|
|
resolvePluginSetupRegistry({
|
|
env: {},
|
|
pluginIds: ["openai"],
|
|
});
|
|
resolvePluginSetupRegistry({
|
|
env: {},
|
|
pluginIds: ["anthropic"],
|
|
});
|
|
expect(setupRegistryTesting.getCacheSizes().setupRegistry).toBe(1);
|
|
expect(loadSetupModule).toHaveBeenCalledTimes(7);
|
|
});
|
|
});
|