


























@@ -13,6 +13,7 @@ import type {
1313AcpRuntimeHandle,
1414AcpRuntimeTurnInput,
1515} from "../../plugin-sdk/acp-runtime.js";
16+import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js";
1617import type {
1718PluginHookBeforeDispatchResult,
1819PluginHookReplyDispatchResult,
@@ -791,6 +792,7 @@ async function dispatchTwiceWithFreshDispatchers(params: Omit<DispatchReplyArgs,
791792describe("dispatchReplyFromConfig", () => {
792793beforeEach(() => {
793794clearAgentHarnesses();
795+clearPluginCommands();
794796const discordTestPlugin = {
795797 ...createChannelTestPluginBase({
796798id: "discord",
@@ -3881,6 +3883,217 @@ describe("dispatchReplyFromConfig", () => {
38813883expect(replyResolver).not.toHaveBeenCalled();
38823884});
388338853886+it("lets authorized plugin-owned binding commands fall through to command processing", async () => {
3887+setNoAbort();
3888+expect(
3889+registerPluginCommand(
3890+"codex",
3891+{
3892+name: "codex",
3893+description: "Control Codex app-server bindings",
3894+acceptsArgs: true,
3895+requireAuth: true,
3896+handler: vi.fn(async () => ({ continueAgent: true })),
3897+},
3898+{ allowReservedCommandNames: true },
3899+),
3900+).toEqual({ ok: true });
3901+hookMocks.runner.hasHooks.mockImplementation(
3902+((hookName?: string) =>
3903+hookName === "inbound_claim" || hookName === "message_received") as () => boolean,
3904+);
3905+hookMocks.registry.plugins = [{ id: "openclaw-codex-app-server", status: "loaded" }];
3906+hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({
3907+status: "handled",
3908+result: { handled: true },
3909+});
3910+sessionBindingMocks.resolveByConversation.mockReturnValue({
3911+bindingId: "binding-command-escape-1",
3912+targetSessionKey: "plugin-binding:codex:abc123",
3913+targetKind: "session",
3914+conversation: {
3915+channel: "discord",
3916+accountId: "default",
3917+conversationId: "channel:1481858418548412579",
3918+},
3919+status: "active",
3920+boundAt: 1710000000000,
3921+metadata: {
3922+pluginBindingOwner: "plugin",
3923+pluginId: "openclaw-codex-app-server",
3924+pluginRoot: "/Users/huntharo/github/openclaw-app-server",
3925+detachHint: "/codex detach",
3926+data: {
3927+kind: "codex-app-server-session",
3928+version: 1,
3929+sessionFile: "/tmp/session.jsonl",
3930+workspaceDir: "/workspace/openclaw",
3931+},
3932+},
3933+} satisfies SessionBindingRecord);
3934+const cfg = emptyConfig;
3935+const dispatcher = createDispatcher();
3936+const ctx = buildTestCtx({
3937+Provider: "discord",
3938+Surface: "discord",
3939+OriginatingChannel: "discord",
3940+OriginatingTo: "discord:channel:1481858418548412579",
3941+To: "discord:channel:1481858418548412579",
3942+AccountId: "default",
3943+SenderId: "user-9",
3944+SenderUsername: "ada",
3945+CommandSource: "text",
3946+CommandAuthorized: true,
3947+WasMentioned: false,
3948+CommandBody: "/codex detach",
3949+RawBody: "/codex detach",
3950+Body: "/codex detach",
3951+MessageSid: "msg-claim-plugin-command-escape",
3952+SessionKey: "agent:main:discord:channel:1481858418548412579",
3953+});
3954+const replyResolver = vi.fn(async () => ({ text: "detached" }) satisfies ReplyPayload);
3955+3956+const result = await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
3957+3958+expect(result).toEqual({ queuedFinal: true, counts: { tool: 0, block: 0, final: 0 } });
3959+expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-command-escape-1");
3960+expect(hookMocks.runner.runInboundClaimForPluginOutcome).not.toHaveBeenCalled();
3961+expect(hookMocks.runner.runInboundClaim).not.toHaveBeenCalled();
3962+expect(replyResolver).toHaveBeenCalledTimes(1);
3963+expect(firstFinalReplyPayload(dispatcher)?.text).toBe("detached");
3964+});
3965+3966+it("keeps authorized unknown slash text in a plugin-owned binding routed to the bound plugin", async () => {
3967+setNoAbort();
3968+hookMocks.runner.hasHooks.mockImplementation(
3969+((hookName?: string) =>
3970+hookName === "inbound_claim" || hookName === "message_received") as () => boolean,
3971+);
3972+hookMocks.registry.plugins = [{ id: "openclaw-codex-app-server", status: "loaded" }];
3973+hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({
3974+status: "handled",
3975+result: { handled: true },
3976+});
3977+sessionBindingMocks.resolveByConversation.mockReturnValue({
3978+bindingId: "binding-command-unknown-slash",
3979+targetSessionKey: "plugin-binding:codex:abc123",
3980+targetKind: "session",
3981+conversation: {
3982+channel: "discord",
3983+accountId: "default",
3984+conversationId: "channel:1481858418548412579",
3985+},
3986+status: "active",
3987+boundAt: 1710000000000,
3988+metadata: {
3989+pluginBindingOwner: "plugin",
3990+pluginId: "openclaw-codex-app-server",
3991+pluginRoot: "/Users/huntharo/github/openclaw-app-server",
3992+},
3993+} satisfies SessionBindingRecord);
3994+const cfg = emptyConfig;
3995+const dispatcher = createDispatcher();
3996+const ctx = buildTestCtx({
3997+Provider: "discord",
3998+Surface: "discord",
3999+OriginatingChannel: "discord",
4000+OriginatingTo: "discord:channel:1481858418548412579",
4001+To: "discord:channel:1481858418548412579",
4002+AccountId: "default",
4003+SenderId: "user-9",
4004+SenderUsername: "ada",
4005+CommandSource: "text",
4006+CommandAuthorized: true,
4007+WasMentioned: false,
4008+CommandBody: "/notes keep this with the bound plugin",
4009+RawBody: "/notes keep this with the bound plugin",
4010+Body: "/notes keep this with the bound plugin",
4011+MessageSid: "msg-claim-plugin-command-unknown-slash",
4012+SessionKey: "agent:main:discord:channel:1481858418548412579",
4013+});
4014+const replyResolver = vi.fn(async () => ({ text: "should not run" }) satisfies ReplyPayload);
4015+4016+const result = await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
4017+4018+expect(result).toEqual({ queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } });
4019+expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-command-unknown-slash");
4020+expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledWith(
4021+"openclaw-codex-app-server",
4022+expect.objectContaining({ content: "/notes keep this with the bound plugin" }),
4023+expect.objectContaining({
4024+pluginBinding: expect.objectContaining({ bindingId: "binding-command-unknown-slash" }),
4025+}),
4026+);
4027+expect(hookMocks.runner.runInboundClaim).not.toHaveBeenCalled();
4028+expect(replyResolver).not.toHaveBeenCalled();
4029+});
4030+4031+it("keeps unauthorized plugin-owned binding slash text routed to the bound plugin", async () => {
4032+setNoAbort();
4033+hookMocks.runner.hasHooks.mockImplementation(
4034+((hookName?: string) =>
4035+hookName === "inbound_claim" || hookName === "message_received") as () => boolean,
4036+);
4037+hookMocks.registry.plugins = [{ id: "openclaw-codex-app-server", status: "loaded" }];
4038+hookMocks.runner.runInboundClaimForPluginOutcome.mockResolvedValue({
4039+status: "handled",
4040+result: { handled: true },
4041+});
4042+sessionBindingMocks.resolveByConversation.mockReturnValue({
4043+bindingId: "binding-command-escape-denied",
4044+targetSessionKey: "plugin-binding:codex:abc123",
4045+targetKind: "session",
4046+conversation: {
4047+channel: "discord",
4048+accountId: "default",
4049+conversationId: "channel:1481858418548412579",
4050+},
4051+status: "active",
4052+boundAt: 1710000000000,
4053+metadata: {
4054+pluginBindingOwner: "plugin",
4055+pluginId: "openclaw-codex-app-server",
4056+pluginRoot: "/Users/huntharo/github/openclaw-app-server",
4057+detachHint: "/codex detach",
4058+},
4059+} satisfies SessionBindingRecord);
4060+const cfg = emptyConfig;
4061+const dispatcher = createDispatcher();
4062+const ctx = buildTestCtx({
4063+Provider: "discord",
4064+Surface: "discord",
4065+OriginatingChannel: "discord",
4066+OriginatingTo: "discord:channel:1481858418548412579",
4067+To: "discord:channel:1481858418548412579",
4068+AccountId: "default",
4069+SenderId: "user-9",
4070+SenderUsername: "ada",
4071+CommandSource: "text",
4072+CommandAuthorized: false,
4073+WasMentioned: false,
4074+CommandBody: "/codex detach",
4075+RawBody: "/codex detach",
4076+Body: "/codex detach",
4077+MessageSid: "msg-claim-plugin-command-denied",
4078+SessionKey: "agent:main:discord:channel:1481858418548412579",
4079+});
4080+const replyResolver = vi.fn(async () => ({ text: "should not run" }) satisfies ReplyPayload);
4081+4082+const result = await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
4083+4084+expect(result).toEqual({ queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } });
4085+expect(sessionBindingMocks.touch).toHaveBeenCalledWith("binding-command-escape-denied");
4086+expect(hookMocks.runner.runInboundClaimForPluginOutcome).toHaveBeenCalledWith(
4087+"openclaw-codex-app-server",
4088+expect.objectContaining({ content: "/codex detach" }),
4089+expect.objectContaining({
4090+pluginBinding: expect.objectContaining({ bindingId: "binding-command-escape-denied" }),
4091+}),
4092+);
4093+expect(hookMocks.runner.runInboundClaim).not.toHaveBeenCalled();
4094+expect(replyResolver).not.toHaveBeenCalled();
4095+});
4096+38844097it("delivers plugin-owned binding replies returned by the owning inbound claim hook", async () => {
38854098setNoAbort();
38864099hookMocks.runner.hasHooks.mockImplementation(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。