Files
openclaw/extensions/codex/src/app-server/plugin-thread-config.test.ts
Kevin Lin d9b5afad18 fix(codex): rename destructive approval mode to ask (#98501)
* fix(codex): rename destructive approval mode to ask

* fix(codex): keep ask reviewer app-scoped

* test: fix current-main CI regressions
2026-07-01 15:14:55 -07:00

1656 lines
49 KiB
TypeScript

// Codex tests cover plugin thread config plugin behavior.
import { describe, expect, it, vi } from "vitest";
import { CodexAppInventoryCache } from "./app-inventory-cache.js";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "./config.js";
import {
buildCodexPluginAppsConfigPatchFromPolicyContext,
buildCodexPluginThreadConfig,
buildCodexPluginThreadConfigInputFingerprint,
isCodexPluginThreadBindingStale,
mergeCodexThreadConfigs,
shouldBuildCodexPluginThreadConfig,
} from "./plugin-thread-config.js";
import type { v2 } from "./protocol.js";
describe("Codex plugin thread config", () => {
it("defaults destructive app access on 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,
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: "auto",
},
},
});
expect(config.configPatch).not.toHaveProperty("approvals_reviewer");
const apps = config.configPatch?.apps as Record<string, unknown> | undefined;
expect(apps?.["_default"]).not.toHaveProperty("approvals_reviewer");
expect(config.policyContext.apps["google-calendar-app"]).toEqual({
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
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"]).toEqual({
enabled: true,
destructive_enabled: false,
open_world_enabled: true,
default_tools_approval_mode: "auto",
});
expect(disabledApps?.["google-calendar-app"]).not.toHaveProperty("default_tools_enabled");
expect(disabledApps?.["google-calendar-app"]).not.toHaveProperty("approvals_reviewer");
expect(disabledApps?.["google-calendar-app"]).not.toHaveProperty("tools");
expect(
pluginOverrideDisabled.policyContext.apps["google-calendar-app"]?.allowDestructiveActions,
).toBe(false);
expect(
pluginOverrideDisabled.policyContext.apps["google-calendar-app"]?.destructiveApprovalMode,
).toBe("deny");
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"]).toEqual({
enabled: true,
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
});
expect(enabledApps?.["google-calendar-app"]).not.toHaveProperty("approvals_reviewer");
expect(
pluginOverrideEnabled.policyContext.apps["google-calendar-app"]?.allowDestructiveActions,
).toBe(true);
expect(
pluginOverrideEnabled.policyContext.apps["google-calendar-app"]?.destructiveApprovalMode,
).toBe("allow");
});
it("exposes destructive app access while marking auto approval mode", async () => {
const config = await buildReadyGoogleCalendarThreadConfig({
codexPlugins: {
enabled: true,
allow_destructive_actions: "auto",
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
});
const apps = config.configPatch?.apps as Record<string, unknown> | undefined;
expect(apps?.["google-calendar-app"]).toEqual({
enabled: true,
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
});
expect(apps?.["google-calendar-app"]).not.toHaveProperty("approvals_reviewer");
expect(config.policyContext.apps["google-calendar-app"]).toMatchObject({
allowDestructiveActions: true,
destructiveApprovalMode: "auto",
});
});
it("routes destructive approvals to the user while clearing durable overrides for always mode", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
let configReadCount = 0;
const request = vi.fn(async (method: string) => {
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"],
);
}
if (method === "config/read") {
configReadCount += 1;
if (configReadCount > 1) {
return {
config: {
apps: {
"google-calendar-app": {
tools: {
"calendar/read": {
enabled: false,
},
},
},
},
},
};
}
return {
config: {
apps: {
"google-calendar-app": {
tools: {
"calendar/create": {
approval_mode: "approve",
enabled: false,
},
"calendar/read": {
enabled: false,
},
"calendar/update": {
approvalMode: "approve",
},
},
},
},
},
};
}
if (method === "config/value/write") {
return {};
}
throw new Error(`unexpected request ${method}`);
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
allow_destructive_actions: "ask",
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
nowMs: 1,
request,
});
const apps = config.configPatch?.apps as Record<string, unknown> | undefined;
expect(apps?.["google-calendar-app"]).toEqual({
enabled: true,
approvals_reviewer: "user",
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
});
expect(config.configPatch).not.toHaveProperty("approvals_reviewer");
expect(config.policyContext.apps["google-calendar-app"]).toMatchObject({
allowDestructiveActions: true,
destructiveApprovalMode: "ask",
});
expect(request).toHaveBeenCalledWith("config/read", { includeLayers: false });
expect(request.mock.calls.filter(([method]) => method === "config/read")).toHaveLength(2);
expect(request).toHaveBeenCalledWith("config/value/write", {
keyPath: 'apps."google-calendar-app".tools."calendar/create".approval_mode',
value: null,
mergeStrategy: "replace",
});
expect(request).toHaveBeenCalledWith("config/value/write", {
keyPath: 'apps."google-calendar-app".tools."calendar/update".approval_mode',
value: null,
mergeStrategy: "replace",
});
expect(request).not.toHaveBeenCalledWith("config/value/write", {
keyPath: 'apps."google-calendar-app".tools',
value: null,
mergeStrategy: "replace",
});
});
it.each([
["auto", "auto", undefined],
["boolean true", true, undefined],
["boolean false", false, undefined],
["ask", "ask", "user"],
] as const)(
"applies the resolved per-plugin %s reviewer policy over global ask",
async (_name, pluginOverride, expectedReviewer) => {
const config = await buildReadyGoogleCalendarThreadConfig({
codexPlugins: {
enabled: true,
allow_destructive_actions: "ask",
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allow_destructive_actions: pluginOverride,
},
},
},
});
const apps = config.configPatch?.apps as Record<string, unknown> | undefined;
const app = apps?.["google-calendar-app"] as Record<string, unknown> | undefined;
expect(app?.approvals_reviewer).toBe(expectedReviewer);
expect(config.policyContext.apps["google-calendar-app"]?.destructiveApprovalMode).toBe(
pluginOverride === true ? "allow" : pluginOverride === false ? "deny" : pluginOverride,
);
},
);
it("rebuilds persisted app policy with the same reviewer precedence", () => {
const configPatch = buildCodexPluginAppsConfigPatchFromPolicyContext({
fingerprint: "policy",
apps: {
"ask-app": {
configKey: "ask",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "ask",
allowDestructiveActions: true,
destructiveApprovalMode: "ask",
mcpServerNames: ["ask"],
},
"auto-app": {
configKey: "auto",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "auto",
allowDestructiveActions: true,
destructiveApprovalMode: "auto",
mcpServerNames: ["auto"],
},
},
pluginAppIds: {
ask: ["ask-app"],
auto: ["auto-app"],
},
});
expect(configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
"ask-app": {
enabled: true,
approvals_reviewer: "user",
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
},
"auto-app": {
enabled: true,
destructive_enabled: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
},
},
});
expect(configPatch).not.toHaveProperty("approvals_reviewer");
});
it("omits ask policy apps when cwd effective approval overrides remain after cleanup", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
let configReadCount = 0;
const request = vi.fn(async (method: string) => {
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"],
);
}
if (method === "config/read") {
configReadCount += 1;
return {
config: {
apps: {
"google-calendar-app": {
tools: {
"calendar/create": {
approval_mode: "approve",
source: configReadCount === 1 ? "user" : "project",
},
},
},
},
},
};
}
if (method === "config/value/write") {
return { status: "ok" };
}
throw new Error(`unexpected request ${method}`);
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
allow_destructive_actions: "ask",
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
configCwd: "/repo/project",
nowMs: 1,
request,
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(request).toHaveBeenCalledWith("config/read", {
includeLayers: false,
cwd: "/repo/project",
});
expect(request.mock.calls.filter(([method]) => method === "config/read")).toHaveLength(2);
expect(config.diagnostics).toStrictEqual([
{
code: "approval_overrides_clear_failed",
plugin: {
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
enabled: true,
allowDestructiveActions: true,
destructiveApprovalMode: "ask",
},
message:
"Could not clear durable Codex app approval overrides for google-calendar-app: effective approval overrides remain for calendar/create",
},
]);
});
it("omits ask policy apps when approval override writes are overridden", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true)],
nextCursor: null,
}),
});
const request = vi.fn(async (method: string) => {
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"],
);
}
if (method === "config/read") {
return {
config: {
apps: {
"google-calendar-app": {
tools: {
"calendar/create": {
approval_mode: "approve",
},
},
},
},
},
};
}
if (method === "config/value/write") {
return { status: "okOverridden" };
}
throw new Error(`unexpected request ${method}`);
});
const config = await buildCodexPluginThreadConfig({
pluginConfig: {
codexPlugins: {
enabled: true,
allow_destructive_actions: "ask",
plugins: {
"google-calendar": {
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
},
},
},
},
appCache,
appCacheKey: "runtime",
configCwd: "/repo/project",
nowMs: 1,
request,
});
expect(config.configPatch).toEqual({
apps: {
_default: {
enabled: false,
destructive_enabled: false,
open_world_enabled: false,
},
},
});
expect(config.policyContext.apps).toStrictEqual({});
expect(config.diagnostics).toStrictEqual([
{
code: "approval_overrides_clear_failed",
plugin: {
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
enabled: true,
allowDestructiveActions: true,
destructiveApprovalMode: "ask",
},
message:
"Could not clear durable Codex app approval overrides for google-calendar-app: approval override for calendar/create is controlled by another config layer",
},
]);
});
it("omits ask policy apps when durable approval override cleanup fails", 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: "ask",
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"],
);
}
if (method === "config/read") {
throw new Error("readonly config");
}
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).toStrictEqual([
{
code: "approval_overrides_clear_failed",
plugin: {
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
enabled: true,
allowDestructiveActions: true,
destructiveApprovalMode: "ask",
},
message:
"Could not clear durable Codex app approval overrides for google-calendar-app: readonly config",
},
]);
});
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 appListParams: v2.AppsListParams[] = [];
const request = vi.fn(async (method: string, params?: unknown) => {
if (method === "app/list") {
appListParams.push(params as v2.AppsListParams);
return { data: [appInfo("google-calendar-app", true, false)], 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: true,
open_world_enabled: true,
default_tools_approval_mode: "auto",
},
},
});
expect(config.policyContext.apps["google-calendar-app"]).toEqual({
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
mcpServerNames: [],
});
expect(config.diagnostics).toStrictEqual([]);
expect(
request.mock.calls.reduce((count, [method]) => count + (method === "app/list" ? 1 : 0), 0),
).toBe(1);
expect(appListParams).toEqual([
{
cursor: undefined,
limit: 1_000,
forceRefetch: false,
},
]);
});
it("re-enables an OpenClaw-allowed app even when app/list reports it disabled", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [appInfo("google-calendar-app", true, false)],
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.inventory?.records[0]?.apps).toStrictEqual([
{
id: "google-calendar-app",
name: "google-calendar-app",
accessible: true,
enabled: false,
needsAuth: false,
},
]);
expect(config.configPatch?.apps).toMatchObject({
"google-calendar-app": {
enabled: true,
},
});
expect(config.diagnostics).toStrictEqual([]);
});
it("refreshes missing app inventory when plugin activation becomes unnecessary", async () => {
const appCache = new CodexAppInventoryCache();
const appListParams: v2.AppsListParams[] = [];
let pluginListCalls = 0;
const request = vi.fn(async (method: string, params?: unknown) => {
if (method === "plugin/list") {
pluginListCalls += 1;
const active = pluginListCalls > 1;
return pluginList([
pluginSummary("google-calendar", { installed: active, enabled: active }),
]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
if (method === "app/list") {
appListParams.push(params as v2.AppsListParams);
return {
data: [appInfo("google-calendar-app", true)],
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",
request,
});
expect(config.configPatch?.apps).toMatchObject({
"google-calendar-app": {
enabled: true,
},
});
expect(request.mock.calls.map(([method]) => method)).not.toContain("plugin/install");
expect(appListParams).toEqual([
{
cursor: undefined,
limit: 1_000,
forceRefetch: true,
},
]);
});
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).toStrictEqual([
{
code: "app_not_ready",
plugin: {
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
enabled: true,
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
},
message: "google-calendar-app is not accessible for google-calendar.",
},
]);
});
it("does not expose apps for plugins that OpenClaw policy leaves 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": {
enabled: false,
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 })]);
}
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).toStrictEqual([]);
});
it("force-refreshes app inventory when proven plugin apps are not ready", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () => ({
data: [],
nextCursor: null,
}),
});
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: true })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
if (method === "app/list") {
appListParams.push(params as v2.AppsListParams);
return {
data: [appInfo("google-calendar-app", true)],
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).not.toHaveProperty("approvals_reviewer");
expect(config.configPatch?.apps).toEqual({
_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: "auto",
},
});
expect(config.policyContext.apps["google-calendar-app"]).toEqual({
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
mcpServerNames: [],
});
expect(config.diagnostics).toStrictEqual([]);
expect(appListParams).toEqual([
{
cursor: undefined,
limit: 1_000,
forceRefetch: true,
},
]);
});
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).not.toHaveProperty("approvals_reviewer");
expect(config.configPatch?.apps).toEqual({
_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: "auto",
},
});
expect(config.policyContext.apps["google-calendar-app"]).toEqual({
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
mcpServerNames: [],
});
expect(config.diagnostics).toStrictEqual([]);
expect(request.mock.calls.map(([method]) => method)).toEqual([
"plugin/list",
"plugin/read",
"plugin/list",
"plugin/install",
"plugin/list",
"skills/list",
"hooks/list",
"config/mcpServer/reload",
"app/list",
"app/list",
"plugin/list",
"plugin/read",
]);
expect(appListParams).toEqual([
{
cursor: undefined,
limit: 1_000,
forceRefetch: true,
},
{
cursor: undefined,
limit: 1_000,
forceRefetch: true,
},
]);
});
it("installs an unconfigured remote plugin before waiting for app inventory", async () => {
const appCache = new CodexAppInventoryCache();
let installed = false;
const request = vi.fn(async (method: string, params?: unknown) => {
if (method === "plugin/list") {
return pluginList([pluginSummary("google-calendar", { installed, enabled: installed })]);
}
if (method === "plugin/read") {
return pluginDetail("google-calendar", [appSummary("google-calendar-app")]);
}
if (method === "plugin/install") {
installed = 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") {
return {
data: [appInfo("google-calendar-app", true, installed)],
nextCursor: null,
} satisfies v2.AppsListResponse;
}
throw new Error(`unexpected request ${method}: ${JSON.stringify(params)}`);
});
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?.apps).toMatchObject({
"google-calendar-app": {
enabled: true,
},
});
const methods = request.mock.calls.map(([method]) => method);
expect(methods.indexOf("plugin/install")).toBeGreaterThan(-1);
expect(methods.indexOf("app/list")).toBeGreaterThan(methods.indexOf("plugin/install"));
});
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).toHaveLength(1);
expect(config.diagnostics[0]?.code).toBe("plugin_activation_failed");
expect(config.diagnostics[0]?.message).toBe(
"Codex plugin runtime refresh failed after install: 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.policyContext.pluginAppIds).toStrictEqual({
"google-calendar": ["google-calendar-app"],
});
expect(config.diagnostics.map((diagnostic) => diagnostic.code)).toStrictEqual([
"app_inventory_missing",
]);
});
it("fails closed when app inventory entries are malformed", async () => {
const appCache = new CodexAppInventoryCache();
await appCache.refreshNow({
key: "runtime",
nowMs: 0,
request: async () =>
({
data: [{ ...appInfo("google-calendar-app", true), id: "" }] as unknown as v2.AppInfo[],
nextCursor: null,
}) satisfies v2.AppsListResponse,
});
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).toStrictEqual([
{
code: "app_not_ready",
plugin: {
configKey: "google-calendar",
marketplaceName: CODEX_PLUGINS_MARKETPLACE_NAME,
pluginName: "google-calendar",
enabled: true,
allowDestructiveActions: true,
destructiveApprovalMode: "allow",
},
message: "google-calendar-app is not accessible for google-calendar.",
},
]);
});
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: "auto",
});
expect(apps?.["github-app"]).not.toHaveProperty("tools");
});
it("merges app config with native hook config", () => {
expect(
mergeCodexThreadConfigs(
{ "features.hooks": true, hooks: { PreToolUse: [] } },
{ apps: { _default: { enabled: false } } },
),
).toEqual({
"features.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")]);
}
if (method === "config/read") {
return { config: {} };
}
throw new Error(`unexpected request ${method}`);
},
});
}