@dsh-overdrive/gateway 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/adapter.d.ts +7 -1
  2. package/dist/adapters/cli.d.ts +4 -1
  3. package/dist/adapters/cli.js +1 -1
  4. package/dist/adapters/cli.js.map +1 -1
  5. package/dist/adapters/dingtalk.d.ts +30 -1
  6. package/dist/adapters/dingtalk.js +97 -12
  7. package/dist/adapters/dingtalk.js.map +1 -1
  8. package/dist/adapters/discord.d.ts +4 -1
  9. package/dist/adapters/discord.js +4 -1
  10. package/dist/adapters/discord.js.map +1 -1
  11. package/dist/adapters/feishu.d.ts +13 -1
  12. package/dist/adapters/feishu.js +71 -11
  13. package/dist/adapters/feishu.js.map +1 -1
  14. package/dist/adapters/slack.d.ts +4 -1
  15. package/dist/adapters/slack.js +8 -3
  16. package/dist/adapters/slack.js.map +1 -1
  17. package/dist/adapters/telegram.d.ts +11 -2
  18. package/dist/adapters/telegram.js +22 -5
  19. package/dist/adapters/telegram.js.map +1 -1
  20. package/dist/adapters/wecom.d.ts +6 -1
  21. package/dist/adapters/wecom.js +12 -9
  22. package/dist/adapters/wecom.js.map +1 -1
  23. package/dist/adapters/whatsapp.d.ts +5 -2
  24. package/dist/adapters/whatsapp.js +16 -13
  25. package/dist/adapters/whatsapp.js.map +1 -1
  26. package/dist/asr.d.ts +19 -0
  27. package/dist/asr.js +63 -0
  28. package/dist/asr.js.map +1 -0
  29. package/dist/commands.d.ts +5 -0
  30. package/dist/commands.js +7 -0
  31. package/dist/commands.js.map +1 -1
  32. package/dist/config.d.ts +3 -0
  33. package/dist/config.js +3 -0
  34. package/dist/config.js.map +1 -1
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +45 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/pending-buttons.d.ts +19 -0
  39. package/dist/pending-buttons.js +40 -0
  40. package/dist/pending-buttons.js.map +1 -0
  41. package/dist/session.d.ts +6 -2
  42. package/dist/session.js +8 -3
  43. package/dist/session.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/adapter.ts +8 -1
  46. package/src/adapters/cli.ts +3 -3
  47. package/src/adapters/dingtalk.ts +108 -13
  48. package/src/adapters/discord.ts +6 -3
  49. package/src/adapters/feishu.ts +77 -12
  50. package/src/adapters/slack.ts +14 -4
  51. package/src/adapters/telegram.ts +29 -8
  52. package/src/adapters/wecom.ts +13 -11
  53. package/src/adapters/whatsapp.ts +18 -15
  54. package/src/asr.ts +83 -0
  55. package/src/commands.ts +8 -1
  56. package/src/config.ts +6 -0
  57. package/src/index.ts +49 -3
  58. package/src/pending-buttons.ts +45 -0
  59. package/src/session.ts +9 -3
  60. package/test/adapters.dingtalk.test.ts +40 -1
  61. package/test/adapters.feishu.test.ts +37 -1
  62. package/test/asr.test.ts +77 -0
  63. package/test/commands.test.ts +6 -0
  64. package/test/multi.test.ts +29 -8
  65. package/test/pending-buttons.test.ts +61 -0
  66. package/test/session.test.ts +7 -2
  67. package/test/streaming.test.ts +162 -162
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { PendingButtons, PENDING_BUTTONS_TTL_MS } from '../src/pending-buttons.js';
3
+
4
+ describe('PendingButtons', () => {
5
+ it('数字回复命中并消费', () => {
6
+ const pb = new PendingButtons();
7
+ pb.set('chat-1', [
8
+ { id: 'approve:1', label: '同意' },
9
+ { id: 'reject:1', label: '拒绝' },
10
+ ]);
11
+ expect(pb.match('chat-1', '2')).toEqual({ id: 'reject:1', label: '拒绝' });
12
+ expect(pb.match('chat-1', '1')).toBeUndefined(); // 已消费
13
+ });
14
+
15
+ it('非数字 / 越界不消费,按钮仍有效', () => {
16
+ const pb = new PendingButtons();
17
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
18
+ expect(pb.match('chat-1', 'hello')).toBeUndefined();
19
+ expect(pb.match('chat-1', '5')).toBeUndefined();
20
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
21
+ });
22
+
23
+ it('过期后自动失效(不吞后续数字消息)', () => {
24
+ vi.useFakeTimers();
25
+ try {
26
+ const pb = new PendingButtons(1000);
27
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
28
+ vi.advanceTimersByTime(1001);
29
+ expect(pb.match('chat-1', '1')).toBeUndefined();
30
+ } finally {
31
+ vi.useRealTimers();
32
+ }
33
+ });
34
+
35
+ it('TTL 内不失效', () => {
36
+ vi.useFakeTimers();
37
+ try {
38
+ const pb = new PendingButtons(PENDING_BUTTONS_TTL_MS);
39
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
40
+ vi.advanceTimersByTime(PENDING_BUTTONS_TTL_MS - 1);
41
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
42
+ } finally {
43
+ vi.useRealTimers();
44
+ }
45
+ });
46
+
47
+ it('consume 删除 pending(原生按钮点击)', () => {
48
+ const pb = new PendingButtons();
49
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
50
+ pb.consume('chat-1');
51
+ expect(pb.match('chat-1', '1')).toBeUndefined();
52
+ });
53
+
54
+ it('不同 chat 互不影响', () => {
55
+ const pb = new PendingButtons();
56
+ pb.set('chat-1', [{ id: 'a', label: '同意' }]);
57
+ pb.set('chat-2', [{ id: 'b', label: '拒绝' }]);
58
+ expect(pb.match('chat-2', '1')).toEqual({ id: 'b', label: '拒绝' });
59
+ expect(pb.match('chat-1', '1')).toEqual({ id: 'a', label: '同意' });
60
+ });
61
+ });
@@ -8,9 +8,9 @@ describe('buildSessionKey', () => {
8
8
  });
9
9
 
10
10
  describe('Allowlist', () => {
11
- it('空列表放行所有(开发模式)', () => {
11
+ it('空列表 fail-closed:拒绝所有(生产默认)', () => {
12
12
  const allow = new Allowlist([]);
13
- expect(allow.allows('anything:any:any')).toBe(true);
13
+ expect(allow.allows('anything:any:any')).toBe(false);
14
14
  });
15
15
 
16
16
  it('非空列表只放行白名单条目', () => {
@@ -18,4 +18,9 @@ describe('Allowlist', () => {
18
18
  expect(allow.allows('whatsapp:60123:60123')).toBe(true);
19
19
  expect(allow.allows('whatsapp:99999:99999')).toBe(false);
20
20
  });
21
+
22
+ it('ALLOW_ALL 显式放行所有(开发逃生口)', () => {
23
+ const allow = new Allowlist([], true);
24
+ expect(allow.allows('anything:any:any')).toBe(true);
25
+ });
21
26
  });
@@ -1,162 +1,162 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { DeltaTracker, planOutbound, wireAdapter } from '../src/index.js';
3
- import { HELP_TEXT } from '../src/commands.js';
4
- import { GatewayClient, type ServerEvent } from '@dsh-overdrive/sdk';
5
- import type { Adapter, NormalizedMessage, OutboundPayload } from '../src/adapter.js';
6
-
7
- describe('DeltaTracker(message.delta → 打字指示,complete → 终稿)', () => {
8
- it('首个 delta 触发 typing,重复 delta 不重复触发', () => {
9
- const t = new DeltaTracker();
10
- const typings: string[] = [];
11
- const outputs: string[] = [];
12
- t.onDelta('s1', () => typings.push('s1'));
13
- t.onDelta('s1', () => typings.push('s1'));
14
- expect(typings).toEqual(['s1']);
15
- void outputs;
16
- });
17
-
18
- it('complete 后同会话下一个 turn 的 delta 可再次触发 typing', () => {
19
- const t = new DeltaTracker();
20
- const typings: string[] = [];
21
- t.onDelta('s1', () => typings.push('s1'));
22
- t.onComplete('s1');
23
- t.onDelta('s1', () => typings.push('s1'));
24
- expect(typings).toEqual(['s1', 's1']);
25
- });
26
- });
27
-
28
- describe('planOutbound(trajectory.summary 摘要卡片渲染)', () => {
29
- it('trajectory.summary → formatTrajectorySummary 文本', () => {
30
- const ev: ServerEvent = {
31
- type: 'trajectory.summary', sessionId: 'cli:cli:local', ts: 1,
32
- steps: [{ kind: 'thought', label: '分析' }, { kind: 'tool', label: 'bash' }],
33
- };
34
- const out = planOutbound(ev)!;
35
- expect(out.payload.text).toContain('📋 轨迹(2 步)');
36
- expect(out.payload.text).toContain('🧠 分析');
37
- expect(out.payload.text).toContain('🛠️ bash');
38
- });
39
- });
40
-
41
- /** 可编程 FakeAdapter(带 sendTyping 探测)。 */
42
- class FakeAdapter implements Adapter {
43
- readonly id: string;
44
- readonly sent: Array<{ chatId: string; payload: OutboundPayload }> = [];
45
- readonly typings: string[] = [];
46
- private messageCb?: (msg: NormalizedMessage) => Promise<void> | void;
47
- private replyCb?: (buttonId: string) => Promise<void> | void;
48
- constructor(id: string) { this.id = id; }
49
- async connect(): Promise<void> {}
50
- async send(chatId: string, payload: OutboundPayload): Promise<void> { this.sent.push({ chatId, payload }); }
51
- async sendTyping(chatId: string): Promise<void> { this.typings.push(chatId); }
52
- onMessage(cb: (msg: NormalizedMessage) => void): void { this.messageCb = cb; }
53
- onReply(cb: (buttonId: string) => void): void { this.replyCb = cb; }
54
- emit(msg: NormalizedMessage): Promise<void> | void { return this.messageCb?.(msg); }
55
- }
56
-
57
- /** 假 DSH 客户端:可编程事件推流 + 记录调用。 */
58
- function fakeClient() {
59
- const createTasks: Array<{ sessionId: string; kind: string; prompt: string; schedule?: string }> = [];
60
- const resets: string[] = [];
61
- let eventCb: ((ev: ServerEvent) => void) | undefined;
62
- const client = {
63
- upsertSession: async (req: { platform: string; channel: string; user: string }) =>
64
- ({ sessionId: `${req.platform}:${req.channel}:${req.user}` }),
65
- sendMessage: async () => ({ runId: 'r1' }),
66
- resolveApproval: async () => ({ ok: true }),
67
- createTask: async (req: { sessionId: string; kind: 'subagent' | 'cron'; prompt: string; schedule?: string }) => {
68
- createTasks.push(req);
69
- return { taskId: 't1' };
70
- },
71
- resetSession: async (sessionId: string) => { resets.push(sessionId); return { ok: true }; },
72
- connect: async (cb: (ev: ServerEvent) => void) => { eventCb = cb; return () => undefined; },
73
- };
74
- const push = (ev: ServerEvent): void => eventCb?.(ev);
75
- return { client, createTasks, resets, push } as unknown as {
76
- client: GatewayClient; createTasks: typeof createTasks; resets: string[];
77
- push: (ev: ServerEvent) => void;
78
- };
79
- }
80
-
81
- describe('wireAdapter(命令分发 + delta 打字指示 + 轨迹聚合接线)', () => {
82
- it('message.delta → sendTyping 一次;complete 后下一 turn 再触发', async () => {
83
- const adapter = new FakeAdapter('cli');
84
- const { client, push } = fakeClient();
85
- await wireAdapter(adapter, client, { allowlist: [] });
86
-
87
- push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 1, text: '…' });
88
- push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 2, text: '…' });
89
- expect(adapter.typings).toEqual(['cli']); // sendTyping 目标是 chatId(无消息时回退到 channel)
90
- expect(adapter.sent).toHaveLength(0); // delta 不产出文本
91
-
92
- push({ type: 'message.complete', sessionId: 'cli:cli:local', ts: 3, text: '结果' });
93
- push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 4, text: '…' });
94
- expect(adapter.typings).toEqual(['cli', 'cli']);
95
- });
96
-
97
- it('trajectory.step 不实时输出,idle 时以 trajectory.summary 摘要输出', async () => {
98
- const adapter = new FakeAdapter('cli');
99
- const { client, push } = fakeClient();
100
- await wireAdapter(adapter, client, { allowlist: [] });
101
-
102
- push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 1, status: 'busy' });
103
- push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 2, step: { kind: 'thought', label: '分析' } });
104
- push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 3, step: { kind: 'tool', label: 'bash' } });
105
- expect(adapter.sent).toHaveLength(0); // 单步不推
106
-
107
- push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 4, status: 'idle' });
108
- const summary = adapter.sent.find((s) => s.payload.text.includes('📋 轨迹'));
109
- expect(summary?.payload.text).toContain('🧠 分析');
110
- expect(summary?.payload.text).toContain('🛠️ bash');
111
- });
112
-
113
- it('/help → HELP_TEXT 原样输出', async () => {
114
- const adapter = new FakeAdapter('cli');
115
- const { client } = fakeClient();
116
- await wireAdapter(adapter, client, { allowlist: [] });
117
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/help' });
118
- expect(adapter.sent[0].payload.text).toBe(HELP_TEXT);
119
- });
120
-
121
- it('/task 派子任务并回执;/cron 注册定时任务并回执', async () => {
122
- const adapter = new FakeAdapter('cli');
123
- const { client, createTasks } = fakeClient();
124
- await wireAdapter(adapter, client, { allowlist: [] });
125
-
126
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/task 调研竞品' });
127
- expect(createTasks).toContainEqual({ sessionId: 'cli:cli:local', kind: 'subagent', prompt: '调研竞品' });
128
- expect(adapter.sent.at(-1)!.payload.text).toContain('🤖 子任务已派出');
129
-
130
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/cron 0 8 * * * 每日汇报' });
131
- expect(createTasks).toContainEqual({ sessionId: 'cli:cli:local', kind: 'cron', prompt: '每日汇报', schedule: '0 8 * * *' });
132
- expect(adapter.sent.at(-1)!.payload.text).toContain('⏰ 定时任务已注册');
133
- });
134
-
135
- it('/new 走 resetSession 端点并回执;/trace 显示最近摘要,无则提示', async () => {
136
- const adapter = new FakeAdapter('cli');
137
- const { client, resets, push } = fakeClient();
138
- await wireAdapter(adapter, client, { allowlist: [] });
139
-
140
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/trace' });
141
- expect(adapter.sent.at(-1)!.payload.text).toContain('暂无轨迹');
142
-
143
- push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 1, status: 'busy' });
144
- push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 2, step: { kind: 'thought', label: '分析' } });
145
- push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 3, status: 'idle' });
146
-
147
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/trace' });
148
- expect(adapter.sent.at(-1)!.payload.text).toContain('🧠 分析');
149
-
150
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/new' });
151
- expect(resets).toEqual(['cli:cli:local']);
152
- expect(adapter.sent.at(-1)!.payload.text).toContain('🆕 会话已重置');
153
- });
154
-
155
- it('/agents 返回简化回执', async () => {
156
- const adapter = new FakeAdapter('cli');
157
- const { client } = fakeClient();
158
- await wireAdapter(adapter, client, { allowlist: [] });
159
- await adapter.emit({ chatId: 'cli', userId: 'local', text: '/agents' });
160
- expect(adapter.sent[0].payload.text).toContain('/task 派发');
161
- });
162
- });
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DeltaTracker, planOutbound, wireAdapter } from '../src/index.js';
3
+ import { HELP_TEXT } from '../src/commands.js';
4
+ import { GatewayClient, type ServerEvent } from '@dsh-overdrive/sdk';
5
+ import type { Adapter, NormalizedMessage, OutboundPayload } from '../src/adapter.js';
6
+
7
+ describe('DeltaTracker(message.delta → 打字指示,complete → 终稿)', () => {
8
+ it('首个 delta 触发 typing,重复 delta 不重复触发', () => {
9
+ const t = new DeltaTracker();
10
+ const typings: string[] = [];
11
+ const outputs: string[] = [];
12
+ t.onDelta('s1', () => typings.push('s1'));
13
+ t.onDelta('s1', () => typings.push('s1'));
14
+ expect(typings).toEqual(['s1']);
15
+ void outputs;
16
+ });
17
+
18
+ it('complete 后同会话下一个 turn 的 delta 可再次触发 typing', () => {
19
+ const t = new DeltaTracker();
20
+ const typings: string[] = [];
21
+ t.onDelta('s1', () => typings.push('s1'));
22
+ t.onComplete('s1');
23
+ t.onDelta('s1', () => typings.push('s1'));
24
+ expect(typings).toEqual(['s1', 's1']);
25
+ });
26
+ });
27
+
28
+ describe('planOutbound(trajectory.summary 摘要卡片渲染)', () => {
29
+ it('trajectory.summary → formatTrajectorySummary 文本', () => {
30
+ const ev: ServerEvent = {
31
+ type: 'trajectory.summary', sessionId: 'cli:cli:local', ts: 1,
32
+ steps: [{ kind: 'thought', label: '分析' }, { kind: 'tool', label: 'bash' }],
33
+ };
34
+ const out = planOutbound(ev)!;
35
+ expect(out.payload.text).toContain('📋 轨迹(2 步)');
36
+ expect(out.payload.text).toContain('🧠 分析');
37
+ expect(out.payload.text).toContain('🛠️ bash');
38
+ });
39
+ });
40
+
41
+ /** 可编程 FakeAdapter(带 sendTyping 探测)。 */
42
+ class FakeAdapter implements Adapter {
43
+ readonly id: string;
44
+ readonly sent: Array<{ chatId: string; payload: OutboundPayload }> = [];
45
+ readonly typings: string[] = [];
46
+ private messageCb?: (msg: NormalizedMessage) => Promise<void> | void;
47
+ private replyCb?: (buttonId: string) => Promise<void> | void;
48
+ constructor(id: string) { this.id = id; }
49
+ async connect(): Promise<void> {}
50
+ async send(chatId: string, payload: OutboundPayload): Promise<void> { this.sent.push({ chatId, payload }); }
51
+ async sendTyping(chatId: string): Promise<void> { this.typings.push(chatId); }
52
+ onMessage(cb: (msg: NormalizedMessage) => void): void { this.messageCb = cb; }
53
+ onReply(cb: (buttonId: string) => void): void { this.replyCb = cb; }
54
+ emit(msg: NormalizedMessage): Promise<void> | void { return this.messageCb?.(msg); }
55
+ }
56
+
57
+ /** 假 DSH 客户端:可编程事件推流 + 记录调用。 */
58
+ function fakeClient() {
59
+ const createTasks: Array<{ sessionId: string; kind: string; prompt: string; schedule?: string }> = [];
60
+ const resets: string[] = [];
61
+ let eventCb: ((ev: ServerEvent) => void) | undefined;
62
+ const client = {
63
+ upsertSession: async (req: { platform: string; channel: string; user: string }) =>
64
+ ({ sessionId: `${req.platform}:${req.channel}:${req.user}` }),
65
+ sendMessage: async () => ({ runId: 'r1' }),
66
+ resolveApproval: async () => ({ ok: true }),
67
+ createTask: async (req: { sessionId: string; kind: 'subagent' | 'cron'; prompt: string; schedule?: string }) => {
68
+ createTasks.push(req);
69
+ return { taskId: 't1' };
70
+ },
71
+ resetSession: async (sessionId: string) => { resets.push(sessionId); return { ok: true }; },
72
+ connect: async (cb: (ev: ServerEvent) => void) => { eventCb = cb; return () => undefined; },
73
+ };
74
+ const push = (ev: ServerEvent): void => eventCb?.(ev);
75
+ return { client, createTasks, resets, push } as unknown as {
76
+ client: GatewayClient; createTasks: typeof createTasks; resets: string[];
77
+ push: (ev: ServerEvent) => void;
78
+ };
79
+ }
80
+
81
+ describe('wireAdapter(命令分发 + delta 打字指示 + 轨迹聚合接线)', () => {
82
+ it('message.delta → sendTyping 一次;complete 后下一 turn 再触发', async () => {
83
+ const adapter = new FakeAdapter('cli');
84
+ const { client, push } = fakeClient();
85
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
86
+
87
+ push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 1, text: '…' });
88
+ push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 2, text: '…' });
89
+ expect(adapter.typings).toEqual(['cli']); // sendTyping 目标是 chatId(无消息时回退到 channel)
90
+ expect(adapter.sent).toHaveLength(0); // delta 不产出文本
91
+
92
+ push({ type: 'message.complete', sessionId: 'cli:cli:local', ts: 3, text: '结果' });
93
+ push({ type: 'message.delta', sessionId: 'cli:cli:local', ts: 4, text: '…' });
94
+ expect(adapter.typings).toEqual(['cli', 'cli']);
95
+ });
96
+
97
+ it('trajectory.step 不实时输出,idle 时以 trajectory.summary 摘要输出', async () => {
98
+ const adapter = new FakeAdapter('cli');
99
+ const { client, push } = fakeClient();
100
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
101
+
102
+ push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 1, status: 'busy' });
103
+ push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 2, step: { kind: 'thought', label: '分析' } });
104
+ push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 3, step: { kind: 'tool', label: 'bash' } });
105
+ expect(adapter.sent).toHaveLength(0); // 单步不推
106
+
107
+ push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 4, status: 'idle' });
108
+ const summary = adapter.sent.find((s) => s.payload.text.includes('📋 轨迹'));
109
+ expect(summary?.payload.text).toContain('🧠 分析');
110
+ expect(summary?.payload.text).toContain('🛠️ bash');
111
+ });
112
+
113
+ it('/help → HELP_TEXT 原样输出', async () => {
114
+ const adapter = new FakeAdapter('cli');
115
+ const { client } = fakeClient();
116
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
117
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/help' });
118
+ expect(adapter.sent[0].payload.text).toBe(HELP_TEXT);
119
+ });
120
+
121
+ it('/task 派子任务并回执;/cron 注册定时任务并回执', async () => {
122
+ const adapter = new FakeAdapter('cli');
123
+ const { client, createTasks } = fakeClient();
124
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
125
+
126
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/task 调研竞品' });
127
+ expect(createTasks).toContainEqual({ sessionId: 'cli:cli:local', kind: 'subagent', prompt: '调研竞品' });
128
+ expect(adapter.sent.at(-1)!.payload.text).toContain('🤖 子任务已派出');
129
+
130
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/cron 0 8 * * * 每日汇报' });
131
+ expect(createTasks).toContainEqual({ sessionId: 'cli:cli:local', kind: 'cron', prompt: '每日汇报', schedule: '0 8 * * *' });
132
+ expect(adapter.sent.at(-1)!.payload.text).toContain('⏰ 定时任务已注册');
133
+ });
134
+
135
+ it('/new 走 resetSession 端点并回执;/trace 显示最近摘要,无则提示', async () => {
136
+ const adapter = new FakeAdapter('cli');
137
+ const { client, resets, push } = fakeClient();
138
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
139
+
140
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/trace' });
141
+ expect(adapter.sent.at(-1)!.payload.text).toContain('暂无轨迹');
142
+
143
+ push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 1, status: 'busy' });
144
+ push({ type: 'trajectory.step', sessionId: 'cli:cli:local', ts: 2, step: { kind: 'thought', label: '分析' } });
145
+ push({ type: 'agent.status', sessionId: 'cli:cli:local', ts: 3, status: 'idle' });
146
+
147
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/trace' });
148
+ expect(adapter.sent.at(-1)!.payload.text).toContain('🧠 分析');
149
+
150
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/new' });
151
+ expect(resets).toEqual(['cli:cli:local']);
152
+ expect(adapter.sent.at(-1)!.payload.text).toContain('🆕 会话已重置');
153
+ });
154
+
155
+ it('/agents 返回简化回执', async () => {
156
+ const adapter = new FakeAdapter('cli');
157
+ const { client } = fakeClient();
158
+ await wireAdapter(adapter, client, { allowlist: [], allowAll: true });
159
+ await adapter.emit({ chatId: 'cli', userId: 'local', text: '/agents' });
160
+ expect(adapter.sent[0].payload.text).toContain('/task 派发');
161
+ });
162
+ });