Files
openclaw/extensions/codex/src/app-server/plugin-metadata-cache.test.ts
T
Kimi Yu e1b8150a06 fix(codex): let owners install plugins from discovered marketplaces (#122389)
* fix(codex): allow owner-approved marketplace plugins

* fix(codex): preserve marketplace validation and remove unused exports

* docs(codex): clarify already-installed plugin authorization
2026-08-12 20:29:40 +00:00

615 lines
22 KiB
TypeScript

// Codex tests cover plugin metadata cache behavior.
import { describe, expect, it, vi } from "vitest";
import { CodexPluginMetadataCache } from "./plugin-metadata-cache.js";
import type { v2 } from "./protocol.js";
describe("Codex plugin metadata cache", () => {
it("coalesces and reuses the full successful snapshot", async () => {
const cache = new CodexPluginMetadataCache();
let release: ((response: v2.PluginListResponse) => void) | undefined;
const request = vi.fn(
async () =>
await new Promise<v2.PluginListResponse>((resolve) => {
release = resolve;
}),
);
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
const first = cache.load(params);
const second = cache.load(params);
const response = pluginList("openai-curated-remote", "calendar");
release?.(response);
const [firstSnapshot, secondSnapshot] = await Promise.all([first, second]);
expect(request).toHaveBeenCalledTimes(1);
expect(firstSnapshot).toBe(secondSnapshot);
expect(firstSnapshot.response).toBe(response);
await expect(cache.load(params)).resolves.toBe(firstSnapshot);
expect(request).toHaveBeenCalledTimes(1);
});
it("coalesces installed plugins through the exact Codex 0.146 endpoint", async () => {
const cache = new CodexPluginMetadataCache();
let release: ((response: v2.PluginInstalledResponse) => void) | undefined;
const request = vi.fn(
async () =>
await new Promise<v2.PluginInstalledResponse>((resolve) => {
release = resolve;
}),
);
const requestParams = { cwds: ["/workspace/a"] } satisfies v2.PluginInstalledParams;
const params = {
appCacheKey: "runtime-a",
queryKind: "installed" as const,
requestParams,
request,
};
const first = cache.load(params);
const second = cache.load(params);
expect(request).toHaveBeenCalledExactlyOnceWith("plugin/installed", requestParams);
const response = installedPlugins("workspace-directory", "calendar");
release?.(response);
const [firstSnapshot, secondSnapshot] = await Promise.all([first, second]);
expect(firstSnapshot).toBe(secondSnapshot);
expect(firstSnapshot.response).toBe(response);
expect(cache.read("runtime-a", "installed", requestParams)).toBe(firstSnapshot);
expect(cache.read("runtime-a", "installed")).toBeUndefined();
await expect(cache.load(params)).resolves.toBe(firstSnapshot);
expect(request).toHaveBeenCalledTimes(1);
});
it("keeps installed workspaces, catalog queries, and runtime identities separate", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(
async (
method: "plugin/list" | "plugin/installed",
params: v2.PluginListParams | v2.PluginInstalledParams,
) =>
method === "plugin/installed"
? installedPlugins("workspace-directory", params.cwds?.[0] ?? "home")
: pluginList("openai-curated-remote", "calendar"),
);
const installedRequest = async (
method: "plugin/installed",
params: v2.PluginInstalledParams,
): Promise<v2.PluginInstalledResponse> =>
(await request(method, params)) as v2.PluginInstalledResponse;
const catalogRequest = async (
method: "plugin/list",
params: v2.PluginListParams,
): Promise<v2.PluginListResponse> => (await request(method, params)) as v2.PluginListResponse;
const workspaceA = await cache.load({
appCacheKey: "runtime-a",
queryKind: "installed",
requestParams: { cwds: ["/workspace/a"] },
request: installedRequest,
});
const workspaceB = await cache.load({
appCacheKey: "runtime-a",
queryKind: "installed",
requestParams: { cwds: ["/workspace/b"] },
request: installedRequest,
});
const curated = await cache.load({
appCacheKey: "runtime-a",
queryKind: "curated-global",
requestParams: {},
request: catalogRequest,
});
const otherRuntime = await cache.load({
appCacheKey: "runtime-b",
queryKind: "installed",
requestParams: { cwds: ["/workspace/a"] },
request: installedRequest,
});
expect(request).toHaveBeenCalledTimes(4);
expect(request).toHaveBeenNthCalledWith(1, "plugin/installed", {
cwds: ["/workspace/a"],
});
expect(request).toHaveBeenNthCalledWith(2, "plugin/installed", {
cwds: ["/workspace/b"],
});
expect(request).toHaveBeenNthCalledWith(3, "plugin/list", {});
expect(request).toHaveBeenNthCalledWith(4, "plugin/installed", {
cwds: ["/workspace/a"],
});
expect(cache.read("runtime-a", "installed", { cwds: ["/workspace/a"] })).toBe(workspaceA);
expect(cache.read("runtime-a", "installed", { cwds: ["/workspace/b"] })).toBe(workspaceB);
expect(cache.read("runtime-a", "curated-global")).toBe(curated);
expect(cache.read("runtime-b", "installed", { cwds: ["/workspace/a"] })).toBe(otherRuntime);
});
it("keeps repository-scoped and remote-kind plugin catalogs separate", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async (_method: "plugin/list", params: v2.PluginListParams) =>
pluginList(
params.marketplaceKinds?.includes("shared-with-me")
? "workspace-shared-with-me"
: `repo-${params.cwds?.[0] ?? "home"}`,
"security-review",
),
);
const workspaceA = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams: { cwds: ["/workspace/a"] },
request,
});
const workspaceB = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams: { cwds: ["/workspace/b"] },
request,
});
const shared = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams: { cwds: ["/workspace/a"], marketplaceKinds: ["shared-with-me"] },
request,
});
expect(request).toHaveBeenCalledTimes(3);
expect(cache.read("runtime", "curated-global", { cwds: ["/workspace/a"] })).toBe(workspaceA);
expect(cache.read("runtime", "curated-global", { cwds: ["/workspace/b"] })).toBe(workspaceB);
expect(
cache.read("runtime", "curated-global", {
cwds: ["/workspace/a"],
marketplaceKinds: ["shared-with-me"],
}),
).toBe(shared);
});
it("isolates partial catalog snapshots by the marketplace being resolved", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi
.fn<() => Promise<v2.PluginListResponse>>()
.mockResolvedValueOnce(pluginList("company-tools", "security-review"))
.mockResolvedValueOnce(pluginList("openai-curated-remote", "calendar"));
const requestParams = { cwds: ["/workspace/a"] };
const company = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams,
catalogScope: "company-tools",
request,
});
const curated = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams,
request,
});
expect(company.response.marketplaces[0]?.name).toBe("company-tools");
expect(curated.response.marketplaces[0]?.name).toBe("openai-curated-remote");
expect(request).toHaveBeenCalledTimes(2);
expect(cache.read("runtime", "curated-global", requestParams, "company-tools")).toBe(company);
expect(cache.read("runtime", "curated-global", requestParams)).toBe(curated);
});
it("coalesces equivalent order-independent plugin catalog kinds", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => pluginList("workspace-directory", "calendar"));
const first = await cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams: {
cwds: ["/workspace/a"],
marketplaceKinds: ["shared-with-me", "workspace-directory", "shared-with-me"],
},
request,
});
await expect(
cache.load({
appCacheKey: "runtime",
queryKind: "curated-global",
requestParams: {
cwds: ["/workspace/a"],
marketplaceKinds: ["workspace-directory", "shared-with-me"],
},
request,
}),
).resolves.toBe(first);
expect(request).toHaveBeenCalledTimes(1);
});
it("coalesces omitted and null installed-plugin scope as the same upstream query", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => installedPlugins("openai-curated-remote", "calendar"));
const omitted = await cache.load({
appCacheKey: "runtime-a",
queryKind: "installed",
requestParams: {},
request,
});
const explicitNull = await cache.load({
appCacheKey: "runtime-a",
queryKind: "installed",
requestParams: { cwds: null, installSuggestionPluginNames: null },
request,
});
expect(explicitNull).toBe(omitted);
expect(cache.read("runtime-a", "installed")).toBe(omitted);
expect(request).toHaveBeenCalledExactlyOnceWith("plugin/installed", {});
});
it("keys installed suggestion sets using Codex's actual order-independent contract", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => installedPlugins("workspace-directory", "calendar"));
const load = (installSuggestionPluginNames: string[]) =>
cache.load({
appCacheKey: "runtime-a",
queryKind: "installed",
requestParams: {
cwds: ["/workspace/a"],
installSuggestionPluginNames,
},
request,
});
const first = await load(["calendar", "drive", "calendar"]);
await expect(load(["drive", "calendar"])).resolves.toBe(first);
await load(["calendar"]);
expect(request).toHaveBeenCalledTimes(2);
expect(request).toHaveBeenNthCalledWith(1, "plugin/installed", {
cwds: ["/workspace/a"],
installSuggestionPluginNames: ["calendar", "drive", "calendar"],
});
expect(request).toHaveBeenNthCalledWith(2, "plugin/installed", {
cwds: ["/workspace/a"],
installSuggestionPluginNames: ["calendar"],
});
});
it("does not cache a failed installed-plugin request", async () => {
const cache = new CodexPluginMetadataCache();
const response = installedPlugins("workspace-directory", "calendar");
const request = vi
.fn<() => Promise<v2.PluginInstalledResponse>>()
.mockRejectedValueOnce(new Error("installed plugins unavailable"))
.mockResolvedValueOnce(response);
const requestParams = { cwds: ["/workspace/a"] } satisfies v2.PluginInstalledParams;
const params = {
appCacheKey: "runtime-a",
queryKind: "installed" as const,
requestParams,
request,
};
await expect(cache.load(params)).rejects.toThrow("installed plugins unavailable");
expect(cache.read("runtime-a", "installed", requestParams)).toBeUndefined();
await expect(cache.load(params)).resolves.toMatchObject({ response });
expect(cache.read("runtime-a", "installed", requestParams)?.response).toBe(response);
expect(request).toHaveBeenCalledTimes(2);
});
it("does not cache installed marketplaces that Codex reports as incomplete", async () => {
const cache = new CodexPluginMetadataCache();
const incomplete = installedPlugins("workspace-directory", "calendar");
incomplete.marketplaceLoadErrors.push({
marketplacePath: "/workspace/a/.codex/plugins",
message: "workspace marketplace unavailable",
});
const healthy = installedPlugins("workspace-directory", "calendar");
const request = vi
.fn<() => Promise<v2.PluginInstalledResponse>>()
.mockResolvedValueOnce(incomplete)
.mockResolvedValueOnce(healthy);
const requestParams = { cwds: ["/workspace/a"] } satisfies v2.PluginInstalledParams;
const params = {
appCacheKey: "runtime-a",
queryKind: "installed" as const,
requestParams,
request,
};
await expect(cache.load(params)).resolves.toMatchObject({ response: incomplete });
expect(cache.read("runtime-a", "installed", requestParams)).toBeUndefined();
await expect(cache.load(params)).resolves.toMatchObject({ response: healthy });
expect(cache.read("runtime-a", "installed", requestParams)?.response).toBe(healthy);
expect(request).toHaveBeenCalledTimes(2);
});
it("invalidates every installed workspace snapshot for the changed runtime only", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => installedPlugins("workspace-directory", "calendar"));
const load = (appCacheKey: string, cwd: string) =>
cache.load({
appCacheKey,
queryKind: "installed",
requestParams: { cwds: [cwd] },
request,
});
await load("runtime-a", "/workspace/a");
await load("runtime-a", "/workspace/b");
const unrelated = await load("runtime-b", "/workspace/a");
cache.invalidate("runtime-a");
expect(cache.read("runtime-a", "installed", { cwds: ["/workspace/a"] })).toBeUndefined();
expect(cache.read("runtime-a", "installed", { cwds: ["/workspace/b"] })).toBeUndefined();
expect(cache.read("runtime-b", "installed", { cwds: ["/workspace/a"] })).toBe(unrelated);
await load("runtime-a", "/workspace/a");
expect(request).toHaveBeenCalledTimes(4);
});
it("does not settle snapshots the caller marks uncacheable", async () => {
// Upstream plugin/list fails open for remote catalogs (local-only response,
// empty marketplaceLoadErrors); such a snapshot must not settle negatives.
const cache = new CodexPluginMetadataCache();
const failOpen = pluginList("local-only");
const healthy = pluginList("openai-curated-remote", "calendar");
const request = vi.fn(async () => (request.mock.calls.length > 1 ? healthy : failOpen));
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
cacheable: (response: v2.PluginListResponse) =>
response.marketplaces.some((entry) => entry.name === "openai-curated-remote"),
};
await expect(cache.load(params)).resolves.toMatchObject({ response: failOpen });
expect(cache.read("runtime-a", "curated-global")).toBeUndefined();
await expect(cache.load(params)).resolves.toMatchObject({ response: healthy });
expect(cache.read("runtime-a", "curated-global")?.response).toBe(healthy);
expect(request).toHaveBeenCalledTimes(2);
});
it("expires settled snapshots after the freshness window", async () => {
// Upstream refreshes its remote catalog in the background; a settled
// negative must not deny a configured plugin for the process lifetime.
let now = 0;
const cache = new CodexPluginMetadataCache(() => now);
const request = vi.fn(async () => pluginList("openai-curated-remote", "calendar"));
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
await cache.load(params);
await cache.load(params);
expect(request).toHaveBeenCalledTimes(1);
now = 60 * 60 * 1_000 + 1;
expect(cache.read("runtime-a", "curated-global")).toBeUndefined();
await cache.load(params);
expect(request).toHaveBeenCalledTimes(2);
});
it("keeps curated catalog runtime identities separate", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => pluginList("openai-curated-remote"));
await cache.load({
appCacheKey: "runtime-a",
queryKind: "curated-global",
requestParams: {},
request,
});
await cache.load({
appCacheKey: "runtime-b",
queryKind: "curated-global",
requestParams: {},
request,
});
expect(request).toHaveBeenCalledTimes(2);
});
it("does not cache failed requests", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi
.fn<() => Promise<v2.PluginListResponse>>()
.mockRejectedValueOnce(new Error("catalog unavailable"))
.mockResolvedValueOnce(pluginList("openai-curated-remote"));
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
await expect(cache.load(params)).rejects.toThrow("catalog unavailable");
await expect(cache.load(params)).resolves.toMatchObject({
response: { marketplaces: [{ name: "openai-curated-remote" }] },
});
expect(request).toHaveBeenCalledTimes(2);
});
it("does not cache responses with marketplace load errors", async () => {
const cache = new CodexPluginMetadataCache();
const incomplete = pluginList("openai-curated-remote");
incomplete.marketplaceLoadErrors = [
{ marketplacePath: "/marketplaces/openai-curated", message: "catalog unavailable" },
];
const request = vi
.fn<() => Promise<v2.PluginListResponse>>()
.mockResolvedValueOnce(incomplete)
.mockResolvedValueOnce(pluginList("openai-curated-remote", "calendar"));
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
await expect(cache.load(params)).resolves.toMatchObject({ response: incomplete });
expect(cache.read("runtime-a", "curated-global")).toBeUndefined();
await expect(cache.load(params)).resolves.toMatchObject({
response: { marketplaces: [{ plugins: [{ id: "calendar" }] }] },
});
expect(request).toHaveBeenCalledTimes(2);
});
it("starts a fresh load after invalidation while an older load is pending", async () => {
const cache = new CodexPluginMetadataCache();
const releases: Array<(response: v2.PluginListResponse) => void> = [];
const request = vi.fn(
async () =>
await new Promise<v2.PluginListResponse>((resolve) => {
releases.push(resolve);
}),
);
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
const beforeInstall = cache.load(params);
await vi.waitFor(() => expect(releases).toHaveLength(1));
cache.invalidate("runtime-a");
const afterInstall = cache.load(params);
await vi.waitFor(() => expect(releases).toHaveLength(2));
const current = pluginList("openai-curated-remote", "calendar");
releases[1]?.(current);
await expect(afterInstall).resolves.toMatchObject({ response: current });
releases[0]?.(pluginList("openai-curated-remote"));
await expect(beforeInstall).resolves.toBeDefined();
expect(cache.read("runtime-a", "curated-global")?.response).toBe(current);
expect(request).toHaveBeenCalledTimes(2);
});
it("retries a joined load with the caller's request after the owner fails", async () => {
const cache = new CodexPluginMetadataCache();
let rejectOwner: ((error: Error) => void) | undefined;
const ownerRequest = vi.fn(
async () =>
await new Promise<v2.PluginListResponse>((_resolve, reject) => {
rejectOwner = reject;
}),
);
const params = {
appCacheKey: "runtime-a",
queryKind: "curated-global" as const,
requestParams: {},
};
const owner = cache.load({ ...params, request: ownerRequest });
const ownerResult = owner.catch((error: unknown) => error);
await vi.waitFor(() => expect(rejectOwner).toBeTypeOf("function"));
const joiningRequest = vi.fn(async () => pluginList("openai-curated-remote", "calendar"));
const joining = cache.load({ ...params, request: joiningRequest });
rejectOwner?.(new Error("owner cancelled"));
await expect(ownerResult).resolves.toBeInstanceOf(Error);
await expect(joining).resolves.toMatchObject({
response: { marketplaces: [{ plugins: [{ id: "calendar" }] }] },
});
expect(ownerRequest).toHaveBeenCalledTimes(1);
expect(joiningRequest).toHaveBeenCalledTimes(1);
});
it("keeps an unrelated runtime load cacheable across invalidation", async () => {
const cache = new CodexPluginMetadataCache();
let release: ((response: v2.PluginListResponse) => void) | undefined;
const request = vi.fn(
async () =>
await new Promise<v2.PluginListResponse>((resolve) => {
release = resolve;
}),
);
const params = {
appCacheKey: "runtime-b",
queryKind: "curated-global" as const,
requestParams: {},
request,
};
const pending = cache.load(params);
await vi.waitFor(() => expect(release).toBeTypeOf("function"));
cache.invalidate("runtime-a");
const response = pluginList("openai-curated-remote", "calendar");
release?.(response);
await pending;
await expect(cache.load(params)).resolves.toMatchObject({ response });
expect(request).toHaveBeenCalledTimes(1);
});
it("invalidates one runtime and clear resets all snapshots", async () => {
const cache = new CodexPluginMetadataCache();
const request = vi.fn(async () => pluginList("openai-curated-remote"));
const load = (appCacheKey: string) =>
cache.load({
appCacheKey,
queryKind: "curated-global",
requestParams: {},
request,
});
await load("runtime-a");
await load("runtime-b");
cache.invalidate("runtime-a");
await load("runtime-a");
await load("runtime-b");
expect(request).toHaveBeenCalledTimes(3);
cache.clear();
expect(cache.read("runtime-a", "curated-global")).toBeUndefined();
expect(cache.read("runtime-b", "curated-global")).toBeUndefined();
});
});
function pluginList(marketplaceName: string, pluginId?: string): v2.PluginListResponse {
return {
marketplaces: [
{
name: marketplaceName,
path: null,
interface: null,
plugins: pluginId
? [
{
id: pluginId,
name: pluginId,
source: { type: "remote" },
installed: false,
enabled: false,
installPolicy: "AVAILABLE",
authPolicy: "ON_USE",
availability: "AVAILABLE",
interface: null,
},
]
: [],
},
],
marketplaceLoadErrors: [],
featuredPluginIds: [],
};
}
function installedPlugins(marketplaceName: string, pluginId?: string): v2.PluginInstalledResponse {
const { marketplaces } = pluginList(marketplaceName, pluginId);
for (const marketplace of marketplaces) {
for (const plugin of marketplace.plugins) {
plugin.installed = true;
plugin.enabled = true;
}
}
return {
marketplaces,
marketplaceLoadErrors: [],
};
}