Files
openclaw/extensions/codex/src/app-server/plugin-thread-config.test.ts
2026-05-09 04:45:12 +01:00

733 lines
22 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { CodexAppInventoryCache } from "./app-inventory-cache.js";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "./config.js";
import {
buildCodexPluginThreadConfig,
buildCodexPluginThreadConfigInputFingerprint,
isCodexPluginThreadBindingStale,
mergeCodexThreadConfigs,
shouldBuildCodexPluginThreadConfig,
} from "./plugin-thread-config.js";
import type { v2 } from "./protocol.js";
describe("Codex plugin thread config", () => {
it("builds restrictive app config for accessible migrated plugin apps", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
allow_destructive_actions: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request: async (method) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail(
"google-calendar",
[appSummary("google-calendar-app")],
["google-calendar"],
);
}
throw new Error(`unexpected request ${method}`);
},
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
"google-calendar-app": {
enabled: true,
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "prompt",
},
},
});
expect(config.policyContext.apps["google-calendar-app"]).toEqual({
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allowDestructiveActions: true,
mcpServerNames: ["google-calendar"],
});
expect(config.diagnostics).toStrictEqual([]);
});
it("maps destructive app access from global and per-plugin policy", async () => {
const pluginOverrideDisabled = await buildReadyGoogleCalendarThreadConfig({
codexPlugins: {
enabled: true,
allow_destructive_actions: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allow_destructive_actions: false,
},
},
},
});
const disabledApps = pluginOverrideDisabled.configPatch?.apps as
| Record<string, unknown>
| undefined;
expect(disabledApps?.["google-calendar-app"]).toMatchObject({
enabled: true,
destructive_enabled: false,
open_world_enabled: true,
});
expect(disabledApps?.["google-calendar-app"]).not.toHaveProperty("default_tools_enabled");
expect(disabledApps?.["google-calendar-app"]).not.toHaveProperty("tools");
expect(
pluginOverrideDisabled.policyContext.apps["google-calendar-app"]?.allowDestructiveActions,
).toBe(false);
const pluginOverrideEnabled = await buildReadyGoogleCalendarThreadConfig({
codexPlugins: {
enabled: true,
allow_destructive_actions: false,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allow_destructive_actions: true,
},
},
},
});
const enabledApps = pluginOverrideEnabled.configPatch?.apps as
| Record<string, unknown>
| undefined;
expect(enabledApps?.["google-calendar-app"]).toMatchObject({
enabled: true,
destructive_enabled: true,
});
expect(
pluginOverrideEnabled.policyContext.apps["google-calendar-app"]?.allowDestructiveActions,
).toBe(true);
});
it("builds a restrictive app config when native plugin support is disabled", async () => {
expect(
shouldBuildCodexPluginThreadConfig({
codexPlugins: { enabled: false },
}),
).toBe(true);
const config = await buildCodexPluginThreadConfig({
pluginConfig: { codexPlugins: { enabled: false } },
appCacheKey: "runtime",
request: async (method) => {
throw new Error(`unexpected request ${method}`);
},
});
expect(config.enabled).toBe(false);
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.diagnostics).toStrictEqual([]);
expect(config.policyContext.apps).toStrictEqual({});
});
it("does not let per-plugin enablement override disabled native plugin support", async () => {
expect(
shouldBuildCodexPluginThreadConfig({
codexPlugins: {
enabled: false,
plugins: {
"google-calendar": {
enabled: true,
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
}),
).toBe(true);
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: false,
plugins: {
"google-calendar": {
enabled: true,
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCacheKey: "runtime",
request: async (method) => {
throw new Error(`unexpected request ${method}`);
},
});
expect(config.enabled).toBe(false);
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(config.diagnostics).toStrictEqual([]);
});
it("waits for the initial app inventory before exposing plugin apps", async () => {
const appCache = new CodexAppInventoryCache();
const request = vi.fn(async (method: string) => {
if (method === "app/list") {
return { data: [appInfo("google-calendar-app", true)], nextCursor: null };
}
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
throw new Error(`unexpected request ${method}`);
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
request,
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
"google-calendar-app": {
enabled: true,
destructive_enabled: false,
open_world_enabled: true,
default_tools_approval_mode: "prompt",
},
},
});
expect(config.policyContext.apps["google-calendar-app"]).toMatchObject({
pluginName: "google-calendar",
});
expect(config.diagnostics).toStrictEqual([]);
expect(
request.mock.calls.reduce((count, [method]) => count + (method === "app/list" ? 1 : 0), 0),
).toBe(1);
});
it("does not expose plugin apps missing from the app inventory snapshot", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [],
nextCursor: null,
}),
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request: async (method) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
throw new Error(`unexpected request ${method}`);
},
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(config.diagnostics).toContainEqual(
expect.objectContaining({
code: "app_not_ready",
message: "google-calendar-app is not accessible or enabled for google-calendar.",
}),
);
});
it("re-reads app readiness after re-enabling an installed plugin", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true, false)],
nextCursor: null,
}),
});
let enabled = false;
const appListParams: v2.AppsListParams[] = [];
const request = vi.fn(async (method: string, params?: unknown) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
if (method === "plugin/install") {
enabled = true;
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
if (method === "skills/list") {
return { data: [] } satisfies v2.SkillsListResponse;
}
if (method === "hooks/list") {
return { data: [] } satisfies v2.HooksListResponse;
}
if (method === "config/mcpServer/reload") {
return {};
}
if (method === "app/list") {
appListParams.push(params as v2.AppsListParams);
return {
data: [appInfo("google-calendar-app", true, enabled)],
nextCursor: null,
} satisfies v2.AppsListResponse;
}
throw new Error(`unexpected request ${method}`);
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request,
});
expect(config.configPatch?.apps).toMatchObject({
"google-calendar-app": {
enabled: true,
destructive_enabled: false,
open_world_enabled: true,
},
});
expect(config.policyContext.apps["google-calendar-app"]).toMatchObject({
pluginName: "google-calendar",
});
expect(config.diagnostics).toStrictEqual([]);
expect(request.mock.calls.map(([method]) => method)).toContain("plugin/install");
expect(request.mock.calls.some(([method]) => method === "app/list")).toBe(true);
expect(appListParams.map((params) => params.forceRefetch)).toContain(true);
});
it("surfaces critical post-install refresh failures and keeps plugin apps disabled", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request: async (method) => {
if (method === "plugin/list") {
return pluginList([
pluginSummary("google-calendar", { installed: false, enabled: false }),
]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
if (method === "plugin/install") {
return { authPolicy: "ON_USE", appsNeedingAuth: [] } satisfies v2.PluginInstallResponse;
}
if (method === "skills/list") {
throw new Error("skills/list unavailable");
}
throw new Error(`unexpected request ${method}`);
},
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(config.diagnostics).toContainEqual(
expect.objectContaining({
code: "plugin_activation_failed",
message: expect.stringContaining("skills/list unavailable"),
}),
);
});
it("fails closed when the initial app inventory refresh fails", async () => {
const appCache = new CodexAppInventoryCache();
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
request: async (method) => {
if (method === "app/list") {
throw new Error("app/list unavailable");
}
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
throw new Error(`unexpected request ${method}`);
},
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(config.diagnostics).toContainEqual(
expect.objectContaining({ code: "app_inventory_missing" }),
);
});
it("uses durable policy and app cache key in the cheap input fingerprint", async () => {
const appCache = new CodexAppInventoryCache();
const first = buildCodexPluginThreadConfigInputFingerprint({
pluginConfig: { codexPlugins: { enabled: true } },
appCacheKey: "runtime-a",
});
await appCache.refreshNow({
key: "runtime-a",
request: async () => ({ data: [], nextCursor: null }),
});
const second = buildCodexPluginThreadConfigInputFingerprint({
pluginConfig: { codexPlugins: { enabled: true } },
appCacheKey: "runtime-a",
});
const third = buildCodexPluginThreadConfigInputFingerprint({
pluginConfig: { codexPlugins: { enabled: true } },
appCacheKey: "runtime-b",
});
expect(second).toBe(first);
expect(third).not.toBe(second);
});
it("uses app-level destructive policy for plugins without OpenClaw tool-name knowledge", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("github-app", true)],
nextCursor: null,
}),
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
allow_destructive_actions: false,
plugins: {
github: {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "github",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request: async (method) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("github", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail("github", [appSummary("github-app")], ["github"]);
}
throw new Error(`unexpected request ${method}`);
},
});
const apps = config.configPatch?.apps as Record<string, unknown> | undefined;
expect(apps?.["github-app"]).toEqual({
enabled: true,
destructive_enabled: false,
open_world_enabled: true,
default_tools_approval_mode: "prompt",
});
expect(apps?.["github-app"]).not.toHaveProperty("tools");
});
it("merges app config with native hook config", () => {
expect(
mergeCodexThreadConfigs(
{ "features.codex_hooks": true, hooks: { PreToolUse: [] } },
{ apps: { _default: { enabled: false } } },
),
).toEqual({
"features.codex_hooks": true,
hooks: { PreToolUse: [] },
apps: { _default: { enabled: false } },
});
});
it("marks missing and changed plugin app bindings stale only when relevant", () => {
expect(
isCodexPluginThreadBindingStale({
codexPluginsEnabled: true,
currentInputFingerprint: "input-2",
}),
).toBe(true);
expect(
isCodexPluginThreadBindingStale({
codexPluginsEnabled: true,
bindingFingerprint: "config-1",
bindingInputFingerprint: "input-1",
currentInputFingerprint: "input-2",
hasBindingPolicyContext: true,
}),
).toBe(true);
expect(
isCodexPluginThreadBindingStale({
codexPluginsEnabled: true,
bindingFingerprint: "config-1",
bindingInputFingerprint: "input-1",
currentInputFingerprint: "input-1",
hasBindingPolicyContext: true,
}),
).toBe(false);
expect(
isCodexPluginThreadBindingStale({
codexPluginsEnabled: false,
bindingFingerprint: "config-1",
bindingInputFingerprint: "input-1",
hasBindingPolicyContext: true,
}),
).toBe(true);
});
});
function pluginList(plugins: v2.PluginSummary[]): v2.PluginListResponse {
return {
marketplaces: [
{
name: CODEX_PLUGINS_MARKETPLACE_NAME,
path: "/marketplaces/openai-curated",
interface: null,
plugins,
},
],
marketplaceLoadErrors: [],
featuredPluginIds: [],
};
}
function pluginSummary(id: string, overrides: Partial<v2.PluginSummary> = {}): v2.PluginSummary {
return {
id,
name: id,
source: { type: "remote" },
installed: false,
enabled: false,
installPolicy: "AVAILABLE",
authPolicy: "ON_USE",
availability: "AVAILABLE",
interface: null,
...overrides,
};
}
function pluginDetail(
pluginName: string,
apps: v2.AppSummary[],
mcpServers: string[] = [],
): v2.PluginReadResponse {
return {
plugin: {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
marketplacePath: "/marketplaces/openai-curated",
summary: pluginSummary(pluginName, { installed: true, enabled: true }),
description: null,
skills: [],
apps,
mcpServers,
},
};
}
function appSummary(id: string): v2.AppSummary {
return {
id,
name: id,
description: null,
installUrl: null,
needsAuth: false,
};
}
function appInfo(id: string, accessible: boolean, enabled = true): v2.AppInfo {
return {
id,
name: id,
description: null,
logoUrl: null,
logoUrlDark: null,
distributionChannel: null,
branding: null,
appMetadata: null,
labels: null,
installUrl: null,
isAccessible: accessible,
isEnabled: enabled,
pluginDisplayNames: [],
};
}
async function buildReadyGoogleCalendarThreadConfig(
pluginConfig: unknown,
): Promise<Awaited<ReturnType<typeof buildCodexPluginThreadConfig>>> {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
return buildCodexPluginThreadConfig({
pluginConfig,
appCache,
appCacheKey: "runtime",
nowMs: 1,
request: async (method) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed: true, enabled: true })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
throw new Error(`unexpected request ${method}`);
},
});
}