


















@@ -0,0 +1,172 @@
1+import type { PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
2+import { describe, expect, it } from "vitest";
3+import {
4+handleCodexPluginsSubcommand,
5+type CodexPluginsConfigBlock,
6+type CodexPluginConfigEntry,
7+type CodexPluginsManagementIO,
8+} from "./command-plugins-management.js";
9+10+function inMemoryIO(
11+initial: Record<string, CodexPluginConfigEntry> = {},
12+options: { enabled?: boolean } = { enabled: true },
13+): CodexPluginsManagementIO & {
14+current: () => Record<string, CodexPluginConfigEntry>;
15+currentConfig: () => CodexPluginsConfigBlock;
16+} {
17+const store: CodexPluginsConfigBlock = {
18+enabled: options.enabled,
19+plugins: JSON.parse(JSON.stringify(initial)),
20+};
21+return {
22+current: () => JSON.parse(JSON.stringify(store.plugins ?? {})),
23+currentConfig: () => JSON.parse(JSON.stringify(store)),
24+readConfig: () => Promise.resolve(JSON.parse(JSON.stringify(store))),
25+mutate: async (update) => {
26+update(store);
27+},
28+};
29+}
30+31+const fakeCtx: PluginCommandContext = {
32+args: "",
33+config: {},
34+channel: "test",
35+isAuthorizedSender: true,
36+senderIsOwner: true,
37+commandBody: "/codex plugins",
38+requestConversationBinding: async () => ({ status: "error", message: "unused" }),
39+detachConversationBinding: async () => ({ removed: false }),
40+getCurrentConversationBinding: async () => null,
41+};
42+43+describe("Codex /codex plugins subcommand", () => {
44+it("lists a configured plugin with its enabled marker and explains the underlying file", async () => {
45+const io = inMemoryIO({
46+"google-calendar": {
47+enabled: true,
48+marketplaceName: "openai-curated",
49+pluginName: "google-calendar",
50+},
51+});
52+53+const result = await handleCodexPluginsSubcommand(fakeCtx, ["list"], io);
54+expect(result.text).toContain("ON google-calendar");
55+expect(result.text).toContain("openclaw.json");
56+});
57+58+it("lists effective disabled status when the global plugin switch is off", async () => {
59+const io = inMemoryIO(
60+{
61+"google-calendar": {
62+enabled: true,
63+marketplaceName: "openai-curated",
64+pluginName: "google-calendar",
65+},
66+},
67+{ enabled: false },
68+);
69+70+const result = await handleCodexPluginsSubcommand(fakeCtx, ["list"], io);
71+expect(result.text).toContain("OFF google-calendar");
72+expect(result.text).toContain("Global codexPlugins.enabled is off");
73+});
74+75+it("enables and disables a configured plugin and reflects the change in subsequent reads", async () => {
76+const io = inMemoryIO({
77+"google-calendar": {
78+enabled: true,
79+marketplaceName: "openai-curated",
80+pluginName: "google-calendar",
81+},
82+});
83+84+const disabled = await handleCodexPluginsSubcommand(
85+fakeCtx,
86+["disable", "google-calendar"],
87+io,
88+);
89+expect(disabled.text).toContain("disabled");
90+expect(io.current()["google-calendar"]?.enabled).toBe(false);
91+92+const enabled = await handleCodexPluginsSubcommand(fakeCtx, ["enable", "google-calendar"], io);
93+expect(enabled.text).toContain("enabled");
94+expect(io.currentConfig().enabled).toBe(true);
95+expect(io.current()["google-calendar"]?.enabled).toBe(true);
96+});
97+98+it("rejects enable and disable from non-owner non-admin callers", async () => {
99+const io = inMemoryIO({
100+"google-calendar": {
101+enabled: true,
102+marketplaceName: "openai-curated",
103+pluginName: "google-calendar",
104+},
105+});
106+const ctx = { ...fakeCtx, senderIsOwner: false, gatewayClientScopes: ["operator.write"] };
107+108+const result = await handleCodexPluginsSubcommand(ctx, ["disable", "google-calendar"], io);
109+expect(result.text).toContain("Only an owner or operator.admin");
110+expect(io.current()["google-calendar"]?.enabled).toBe(true);
111+});
112+113+it("allows operator.admin gateway callers to enable and disable", async () => {
114+const io = inMemoryIO({
115+"google-calendar": {
116+enabled: true,
117+marketplaceName: "openai-curated",
118+pluginName: "google-calendar",
119+},
120+});
121+const ctx = { ...fakeCtx, senderIsOwner: false, gatewayClientScopes: ["operator.admin"] };
122+123+const result = await handleCodexPluginsSubcommand(ctx, ["disable", "google-calendar"], io);
124+expect(result.text).toContain("disabled");
125+expect(io.current()["google-calendar"]?.enabled).toBe(false);
126+});
127+128+it("escapes configured plugin fields before listing them in chat", async () => {
129+const io = inMemoryIO({
130+"google-calendar": {
131+enabled: true,
132+marketplaceName: "openai-curated",
133+pluginName: "google-calendar_@team_*name*",
134+},
135+});
136+137+const result = await handleCodexPluginsSubcommand(fakeCtx, ["list"], io);
138+expect(result.text).toContain("google-calendar");
139+expect(result.text).toContain("google-calendar_@team_∗name∗");
140+expect(result.text).not.toContain("@team");
141+expect(result.text).not.toContain("*name*");
142+});
143+144+it("reports when a target plugin is not configured rather than silently no-oping", async () => {
145+const io = inMemoryIO();
146+const result = await handleCodexPluginsSubcommand(fakeCtx, ["disable", "chrome_@ops"], io);
147+expect(result.text).toContain("not configured");
148+expect(result.text).toContain("chrome_@ops");
149+expect(result.text).not.toContain("@ops");
150+});
151+152+it("returns usage when list, enable, or disable receives the wrong arity", async () => {
153+const io = inMemoryIO();
154+const listResult = await handleCodexPluginsSubcommand(fakeCtx, ["list", "chrome"], io);
155+expect(listResult.text).toContain("Usage: /codex plugins list");
156+157+const result = await handleCodexPluginsSubcommand(fakeCtx, ["disable"], io);
158+expect(result.text).toContain("Usage: /codex plugins disable <name>");
159+expect(result.presentation).toBeUndefined();
160+161+const enableResult = await handleCodexPluginsSubcommand(fakeCtx, ["enable"], io);
162+expect(enableResult.text).toContain("Usage: /codex plugins enable <name>");
163+expect(enableResult.presentation).toBeUndefined();
164+165+const extraResult = await handleCodexPluginsSubcommand(
166+fakeCtx,
167+["enable", "google-calendar", "extra"],
168+io,
169+);
170+expect(extraResult.text).toContain("Usage: /codex plugins enable <name>");
171+});
172+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。