|
1 | 1 | // Clickclack tests cover inbound plugin behavior. |
2 | 2 | import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers"; |
3 | 3 | import type { PluginRuntime } from "openclaw/plugin-sdk/core"; |
| 4 | +import { buildAgentSessionKey, resolveAgentRoute } from "openclaw/plugin-sdk/routing"; |
4 | 5 | import { describe, expect, it, vi } from "vitest"; |
5 | 6 | import { handleClickClackInbound } from "./inbound.js"; |
6 | 7 | import { setClickClackRuntime } from "./runtime.js"; |
@@ -34,28 +35,14 @@ function createRuntime(): PluginRuntime { |
34 | 35 | }, |
35 | 36 | channel: { |
36 | 37 | routing: { |
37 | | - resolveAgentRoute({ |
38 | | - accountId, |
39 | | - peer, |
40 | | - }: Parameters<PluginRuntime["channel"]["routing"]["resolveAgentRoute"]>[0]) { |
41 | | - return { |
42 | | - agentId: "main", |
43 | | - channel: "clickclack", |
44 | | - accountId: accountId ?? "default", |
45 | | - sessionKey: `agent:main:clickclack:${peer?.kind ?? "channel"}:${peer?.id ?? "general"}`, |
46 | | - mainSessionKey: "agent:main:main", |
47 | | - lastRoutePolicy: "session", |
48 | | - matchedBy: "default", |
49 | | - }; |
50 | | - }, |
51 | | - buildAgentSessionKey({ |
52 | | - agentId, |
53 | | - channel, |
54 | | - accountId, |
55 | | - peer, |
56 | | - }: Parameters<PluginRuntime["channel"]["routing"]["buildAgentSessionKey"]>[0]) { |
57 | | - return `agent:${agentId}:${channel}:${accountId ?? "default"}:${peer?.kind ?? "channel"}:${peer?.id ?? "general"}`; |
58 | | - }, |
| 38 | + resolveAgentRoute: vi.fn( |
| 39 | + (params: Parameters<PluginRuntime["channel"]["routing"]["resolveAgentRoute"]>[0]) => |
| 40 | + resolveAgentRoute(params), |
| 41 | + ), |
| 42 | + buildAgentSessionKey: vi.fn( |
| 43 | + (params: Parameters<PluginRuntime["channel"]["routing"]["buildAgentSessionKey"]>[0]) => |
| 44 | + buildAgentSessionKey(params), |
| 45 | + ), |
59 | 46 | }, |
60 | 47 | }, |
61 | 48 | llm: { |
@@ -332,6 +319,87 @@ describe("handleClickClackInbound", () => { |
332 | 319 | expect(dispatchReply.mock.calls[0]?.[0].ctxPayload.CommandAuthorized).toBe(true); |
333 | 320 | }); |
334 | 321 |
|
| 322 | + it("preserves session policy when an account overrides the routed agent", async () => { |
| 323 | + const runtime = createRuntime(); |
| 324 | + setClickClackRuntime(runtime); |
| 325 | + const cfg = { |
| 326 | + session: { |
| 327 | + dmScope: "per-channel-peer", |
| 328 | + mainKey: "work", |
| 329 | + identityLinks: { alice: ["clickclack:dm:usr_owner"] }, |
| 330 | + }, |
| 331 | + bindings: [ |
| 332 | + { |
| 333 | + agentId: "binding-agent", |
| 334 | + match: { |
| 335 | + channel: "clickclack", |
| 336 | + accountId: "default", |
| 337 | + peer: { kind: "direct", id: "dm:usr_owner" }, |
| 338 | + }, |
| 339 | + session: { dmScope: "per-account-channel-peer" }, |
| 340 | + }, |
| 341 | + ], |
| 342 | + } satisfies CoreConfig; |
| 343 | + |
| 344 | + await handleClickClackInbound({ |
| 345 | + account: createAgentAccount({ agentId: "service-bot" }), |
| 346 | + config: cfg, |
| 347 | + message: createMessage({ |
| 348 | + channel_id: undefined, |
| 349 | + direct_conversation_id: "dcn_1", |
| 350 | + }), |
| 351 | + }); |
| 352 | + |
| 353 | + const dispatchReply = vi.mocked(runtime.channel.inbound.dispatchReply); |
| 354 | + expect(dispatchReply.mock.calls[0]?.[0].routeSessionKey).toBe( |
| 355 | + "agent:service-bot:clickclack:direct:alice", |
| 356 | + ); |
| 357 | + expect(runtime.channel.routing.buildAgentSessionKey).toHaveBeenCalledWith({ |
| 358 | + agentId: "service-bot", |
| 359 | + mainKey: "work", |
| 360 | + channel: "clickclack", |
| 361 | + accountId: "default", |
| 362 | + peer: { kind: "direct", id: "dm:usr_owner" }, |
| 363 | + dmScope: "per-channel-peer", |
| 364 | + identityLinks: { alice: ["clickclack:dm:usr_owner"] }, |
| 365 | + }); |
| 366 | + }); |
| 367 | + |
| 368 | + it("preserves binding scope for a canonically equivalent account agent", async () => { |
| 369 | + const runtime = createRuntime(); |
| 370 | + setClickClackRuntime(runtime); |
| 371 | + const cfg = { |
| 372 | + agents: { list: [{ id: "service-bot" }] }, |
| 373 | + session: { dmScope: "main" }, |
| 374 | + bindings: [ |
| 375 | + { |
| 376 | + agentId: "service-bot", |
| 377 | + match: { |
| 378 | + channel: "clickclack", |
| 379 | + accountId: "default", |
| 380 | + peer: { kind: "direct", id: "dm:usr_owner" }, |
| 381 | + }, |
| 382 | + session: { dmScope: "per-account-channel-peer" }, |
| 383 | + }, |
| 384 | + ], |
| 385 | + } satisfies CoreConfig; |
| 386 | + |
| 387 | + await handleClickClackInbound({ |
| 388 | + account: createAgentAccount({ agentId: "SERVICE-BOT" }), |
| 389 | + config: cfg, |
| 390 | + message: createMessage({ |
| 391 | + channel_id: undefined, |
| 392 | + direct_conversation_id: "dcn_1", |
| 393 | + }), |
| 394 | + }); |
| 395 | + |
| 396 | + const dispatchReply = vi.mocked(runtime.channel.inbound.dispatchReply); |
| 397 | + expect(dispatchReply.mock.calls[0]?.[0]).toMatchObject({ |
| 398 | + agentId: "service-bot", |
| 399 | + routeSessionKey: "agent:service-bot:clickclack:default:direct:dm:usr_owner", |
| 400 | + }); |
| 401 | + }); |
| 402 | + |
335 | 403 | it("does not dispatch agent turns from senders outside allowFrom", async () => { |
336 | 404 | const runtime = createRuntime(); |
337 | 405 | vi.mocked(runtime.channel.commands.shouldComputeCommandAuthorized).mockReturnValue(true); |
|
0 commit comments