From 0f38e34becf1890641403902bcb8ad6147984611 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 2 Apr 2026 06:48:29 +0000 Subject: [PATCH] test: cover discussion tool registration flows --- plans/TASKLIST.md | 24 ++--- test/register-tools.test.ts | 208 ++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 test/register-tools.test.ts diff --git a/plans/TASKLIST.md b/plans/TASKLIST.md index d83e814..08b8166 100644 --- a/plans/TASKLIST.md +++ b/plans/TASKLIST.md @@ -133,16 +133,16 @@ - [x] 确保 session 生命周期结束后相关缓存可清理 ### A8. `plugin/core/identity.ts` / `plugin/core/channel-members.ts` / `plugin/core/turn-bootstrap.ts` -- [. ] 梳理 initiator identity 的可获取路径 -- [. ] 确认 callback 时如何稳定识别 initiator account/session -- [. ] 确认 discussion channel 创建后 turn order 是否需立即 bootstrap -- [. ] 确认 discussion participant 的成员集合获取方式是否可直接复用现有逻辑 +- [x] 梳理 initiator identity 的可获取路径 +- [x] 确认 callback 时如何稳定识别 initiator account/session +- [x] 确认 discussion channel 创建后 turn order 是否需立即 bootstrap +- [ ] 确认 discussion participant 的成员集合获取方式是否可直接复用现有逻辑 ### A9. `plugin/index.ts` -- [. ] 注入新增 discussion metadata/service 模块依赖 -- [. ] 将 discussion service 传入工具注册逻辑 -- [. ] 将 discussion 相关辅助能力传入需要的 hooks -- [. ] 保持插件初始化结构清晰,避免在 `index.ts` 中堆业务细节 +- [x] 注入新增 discussion metadata/service 模块依赖 +- [x] 将 discussion service 传入工具注册逻辑 +- [x] 将 discussion 相关辅助能力传入需要的 hooks +- [x] 保持插件初始化结构清晰,避免在 `index.ts` 中堆业务细节 ### A13.2 metadata / service 测试 - [x] 测试 discussion metadata 创建成功 @@ -225,10 +225,10 @@ ### A13. 测试与文档收尾 #### A13.1 工具层测试 -- [. ] 测试普通 `discord_channel_create` 不带新参数时行为不变 -- [. ] 测试 `discord_channel_create` 带 `callbackChannelId` 但缺 `discussGuide` 时失败 -- [. ] 测试 discussion 模式 channel 创建成功 -- [. ] 测试 `discuss-callback` 注册成功并可调用 +- [x] 测试普通 `discord_channel_create` 不带新参数时行为不变 +- [x] 测试 `discord_channel_create` 带 `callbackChannelId` 但缺 `discussGuide` 时失败 +- [x] 测试 discussion 模式 channel 创建成功 +- [x] 测试 `discuss-callback` 注册成功并可调用 #### A13.2 metadata / service 测试 - [x] 测试 discussion metadata 创建成功 diff --git a/test/register-tools.test.ts b/test/register-tools.test.ts new file mode 100644 index 0000000..608dfa2 --- /dev/null +++ b/test/register-tools.test.ts @@ -0,0 +1,208 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { registerDirigentTools } from '../plugin/tools/register-tools.ts'; + +type RegisteredTool = { + name: string; + handler: (params: Record, ctx?: Record) => Promise; +}; + +function makeApi() { + const tools = new Map(); + return { + config: { + channels: { + discord: { + accounts: { + bot: { token: 'discord-bot-token' }, + }, + }, + }, + }, + logger: { + info: (_msg: string) => {}, + warn: (_msg: string) => {}, + }, + registerTool(def: RegisteredTool) { + tools.set(def.name, def); + }, + tools, + }; +} + +function pickDefined(obj: Record) { + return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)); +} + +test('plain private channel create works unchanged without discussion params', async () => { + const api = makeApi(); + let initDiscussionCalls = 0; + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (_url: string | URL | Request, _init?: RequestInit) => { + return new Response(JSON.stringify({ id: 'created-channel-1', name: 'plain-room' }), { status: 200 }); + }) as typeof fetch; + + try { + registerDirigentTools({ + api: api as any, + baseConfig: {}, + pickDefined, + discussionService: { + async initDiscussion() { + initDiscussionCalls += 1; + return {}; + }, + async handleCallback() { + return { ok: true }; + }, + }, + }); + + const tool = api.tools.get('dirigent_discord_control'); + assert.ok(tool); + + const result = await tool!.handler({ + action: 'channel-private-create', + guildId: 'guild-1', + name: 'plain-room', + allowedUserIds: ['user-1'], + }, { + agentId: 'agent-a', + sessionKey: 'session-a', + }); + + assert.equal(result.isError, undefined); + assert.match(result.content[0].text, /"discussionMode": false/); + assert.equal(initDiscussionCalls, 0); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('private channel create rejects callbackChannelId without discussGuide', async () => { + const api = makeApi(); + + registerDirigentTools({ + api: api as any, + baseConfig: {}, + pickDefined, + discussionService: { + async initDiscussion() { + return {}; + }, + async handleCallback() { + return { ok: true }; + }, + }, + }); + + const tool = api.tools.get('dirigent_discord_control'); + assert.ok(tool); + + const result = await tool!.handler({ + action: 'channel-private-create', + guildId: 'guild-1', + name: 'discussion-room', + callbackChannelId: 'origin-1', + }, { + agentId: 'agent-a', + sessionKey: 'session-a', + }); + + assert.equal(result.isError, true); + assert.equal(result.content[0].text, 'discussGuide is required when callbackChannelId is provided'); +}); + +test('discussion-mode channel create initializes discussion metadata', async () => { + const api = makeApi(); + const initCalls: Array> = []; + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (_url: string | URL | Request, _init?: RequestInit) => { + return new Response(JSON.stringify({ id: 'discussion-channel-1', name: 'discussion-room' }), { status: 200 }); + }) as typeof fetch; + + try { + registerDirigentTools({ + api: api as any, + baseConfig: {}, + pickDefined, + discussionService: { + async initDiscussion(params) { + initCalls.push(params as Record); + return {}; + }, + async handleCallback() { + return { ok: true }; + }, + }, + }); + + const tool = api.tools.get('dirigent_discord_control'); + assert.ok(tool); + + const result = await tool!.handler({ + action: 'channel-private-create', + guildId: 'guild-1', + name: 'discussion-room', + callbackChannelId: 'origin-1', + discussGuide: 'Decide the callback contract.', + }, { + agentId: 'agent-a', + sessionKey: 'session-a', + }); + + assert.equal(result.isError, undefined); + assert.match(result.content[0].text, /"discussionMode": true/); + assert.equal(initCalls.length, 1); + assert.deepEqual(initCalls[0], { + discussionChannelId: 'discussion-channel-1', + originChannelId: 'origin-1', + initiatorAgentId: 'agent-a', + initiatorSessionId: 'session-a', + discussGuide: 'Decide the callback contract.', + }); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('discuss-callback registers and forwards channel/session/agent context', async () => { + const api = makeApi(); + const callbackCalls: Array> = []; + + registerDirigentTools({ + api: api as any, + baseConfig: {}, + pickDefined, + discussionService: { + async initDiscussion() { + return {}; + }, + async handleCallback(params) { + callbackCalls.push(params as Record); + return { ok: true, summaryPath: '/workspace/summary.md' }; + }, + }, + }); + + const tool = api.tools.get('discuss-callback'); + assert.ok(tool); + + const result = await tool!.handler({ summaryPath: 'plans/summary.md' }, { + channelId: 'discussion-1', + agentId: 'agent-a', + sessionKey: 'session-a', + }); + + assert.equal(result.isError, undefined); + assert.deepEqual(callbackCalls, [{ + channelId: 'discussion-1', + summaryPath: 'plans/summary.md', + callerAgentId: 'agent-a', + callerSessionKey: 'session-a', + }]); + assert.match(result.content[0].text, /"summaryPath": "\/workspace\/summary.md"/); +});