@ai-setting/roy-agent-cli 1.0.0

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 (158) hide show
  1. package/README.md +126 -0
  2. package/dist/bin/roy.js +127297 -0
  3. package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
  4. package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
  5. package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
  6. package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
  7. package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
  8. package/package.json +91 -0
  9. package/src/bin/roy.ts +12 -0
  10. package/src/cli.ts +101 -0
  11. package/src/commands/act.ts +480 -0
  12. package/src/commands/commands-add.ts +110 -0
  13. package/src/commands/commands-dirs.ts +70 -0
  14. package/src/commands/commands-info.ts +90 -0
  15. package/src/commands/commands-list.ts +161 -0
  16. package/src/commands/commands-remove.ts +147 -0
  17. package/src/commands/commands.ts +55 -0
  18. package/src/commands/config/config-service.test.ts +449 -0
  19. package/src/commands/config/config-service.ts +312 -0
  20. package/src/commands/config/deep-merge.test.ts +168 -0
  21. package/src/commands/config/deep-merge.ts +63 -0
  22. package/src/commands/config/export.ts +97 -0
  23. package/src/commands/config/filter-history-e2e.test.ts +141 -0
  24. package/src/commands/config/import-preserve-refs.test.ts +212 -0
  25. package/src/commands/config/import.ts +119 -0
  26. package/src/commands/config/index.ts +35 -0
  27. package/src/commands/config/list.ts +281 -0
  28. package/src/commands/config/roy-config-e2e.test.ts +297 -0
  29. package/src/commands/config/types.ts +54 -0
  30. package/src/commands/debug/index.ts +38 -0
  31. package/src/commands/debug/log.test.ts +233 -0
  32. package/src/commands/debug/log.ts +123 -0
  33. package/src/commands/debug/span.test.ts +297 -0
  34. package/src/commands/debug/span.ts +211 -0
  35. package/src/commands/debug/trace.test.ts +254 -0
  36. package/src/commands/debug/trace.ts +140 -0
  37. package/src/commands/eventsource/add.ts +133 -0
  38. package/src/commands/eventsource/index.ts +48 -0
  39. package/src/commands/eventsource/list.ts +194 -0
  40. package/src/commands/eventsource/remove.ts +95 -0
  41. package/src/commands/eventsource/start.ts +103 -0
  42. package/src/commands/eventsource/status.ts +185 -0
  43. package/src/commands/eventsource/stop.ts +89 -0
  44. package/src/commands/index.ts +22 -0
  45. package/src/commands/input-handler.test.ts +76 -0
  46. package/src/commands/input-handler.ts +43 -0
  47. package/src/commands/interactive-esc.test.ts +254 -0
  48. package/src/commands/interactive.shutdown.test.ts +122 -0
  49. package/src/commands/interactive.test.ts +221 -0
  50. package/src/commands/interactive.ts +1015 -0
  51. package/src/commands/lsp/check.ts +92 -0
  52. package/src/commands/lsp/index.ts +32 -0
  53. package/src/commands/lsp/install.ts +126 -0
  54. package/src/commands/lsp/list.ts +64 -0
  55. package/src/commands/mcp/index.ts +27 -0
  56. package/src/commands/mcp/list.ts +116 -0
  57. package/src/commands/mcp/reload.ts +70 -0
  58. package/src/commands/mcp/tools.ts +121 -0
  59. package/src/commands/memory/extract-e2e.test.ts +388 -0
  60. package/src/commands/memory/index.ts +11 -0
  61. package/src/commands/memory/memory-simplified.test.ts +58 -0
  62. package/src/commands/memory/memory.ts +25 -0
  63. package/src/commands/memory/organize.ts +300 -0
  64. package/src/commands/memory/recall.test.ts +120 -0
  65. package/src/commands/memory/recall.ts +88 -0
  66. package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
  67. package/src/commands/memory/record-prompt-component.test.ts +343 -0
  68. package/src/commands/memory/record.test.ts +92 -0
  69. package/src/commands/memory/record.ts +332 -0
  70. package/src/commands/plugin.test.ts +292 -0
  71. package/src/commands/plugin.ts +267 -0
  72. package/src/commands/sessions/active.ts +96 -0
  73. package/src/commands/sessions/add-message.ts +96 -0
  74. package/src/commands/sessions/checkpoints.ts +154 -0
  75. package/src/commands/sessions/compact.test.ts +215 -0
  76. package/src/commands/sessions/compact.ts +269 -0
  77. package/src/commands/sessions/delete.ts +236 -0
  78. package/src/commands/sessions/get.ts +165 -0
  79. package/src/commands/sessions/grep.ts +233 -0
  80. package/src/commands/sessions/index.ts +95 -0
  81. package/src/commands/sessions/list.ts +210 -0
  82. package/src/commands/sessions/messages.test.ts +333 -0
  83. package/src/commands/sessions/messages.ts +248 -0
  84. package/src/commands/sessions/mock.ts +194 -0
  85. package/src/commands/sessions/new.ts +82 -0
  86. package/src/commands/sessions/rename.ts +98 -0
  87. package/src/commands/shared/event-handler.ts +213 -0
  88. package/src/commands/shared/event-message-formatter.ts +295 -0
  89. package/src/commands/shared/index.ts +11 -0
  90. package/src/commands/shared/query-executor.test.ts +434 -0
  91. package/src/commands/shared/query-executor.ts +324 -0
  92. package/src/commands/shared/repl-engine.test.ts +354 -0
  93. package/src/commands/shared/session-manager.test.ts +212 -0
  94. package/src/commands/shared/session-manager.ts +114 -0
  95. package/src/commands/skills/get.ts +90 -0
  96. package/src/commands/skills/index.ts +39 -0
  97. package/src/commands/skills/list.ts +129 -0
  98. package/src/commands/skills/reload.ts +59 -0
  99. package/src/commands/skills/search.ts +132 -0
  100. package/src/commands/skills/show-config.ts +93 -0
  101. package/src/commands/tasks/complete.ts +92 -0
  102. package/src/commands/tasks/create.ts +118 -0
  103. package/src/commands/tasks/delete.ts +86 -0
  104. package/src/commands/tasks/get.ts +116 -0
  105. package/src/commands/tasks/index.ts +53 -0
  106. package/src/commands/tasks/list.ts +140 -0
  107. package/src/commands/tasks/operations.ts +120 -0
  108. package/src/commands/tasks/update.ts +122 -0
  109. package/src/commands/tools/exec-tool.ts +128 -0
  110. package/src/commands/tools/get.ts +114 -0
  111. package/src/commands/tools/index.ts +35 -0
  112. package/src/commands/tools/list.ts +107 -0
  113. package/src/commands/tools/shared/index.ts +7 -0
  114. package/src/commands/tools/shared/schema-helper.ts +111 -0
  115. package/src/commands/workflow/commands/add.ts +315 -0
  116. package/src/commands/workflow/commands/get.ts +193 -0
  117. package/src/commands/workflow/commands/list.ts +137 -0
  118. package/src/commands/workflow/commands/nodes.ts +528 -0
  119. package/src/commands/workflow/commands/remove.ts +94 -0
  120. package/src/commands/workflow/commands/run.ts +398 -0
  121. package/src/commands/workflow/commands/status.ts +147 -0
  122. package/src/commands/workflow/commands/stop.ts +91 -0
  123. package/src/commands/workflow/commands/update.ts +130 -0
  124. package/src/commands/workflow/commands/validate.ts +139 -0
  125. package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
  126. package/src/commands/workflow/index.ts +65 -0
  127. package/src/commands/workflow/renderers.ts +358 -0
  128. package/src/commands/workflow/validators/index.ts +8 -0
  129. package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
  130. package/src/commands/workflow/validators/node-validator.ts +125 -0
  131. package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
  132. package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
  133. package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
  134. package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
  135. package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
  136. package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
  137. package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
  138. package/src/commands/workflow/validators/types.ts +78 -0
  139. package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
  140. package/src/commands/workflow/validators/workflow-validator.ts +320 -0
  141. package/src/index.ts +19 -0
  142. package/src/plugin/apply.ts +103 -0
  143. package/src/plugin/discover.ts +219 -0
  144. package/src/plugin/index.ts +45 -0
  145. package/src/plugin/registry.ts +272 -0
  146. package/src/plugin/types.ts +165 -0
  147. package/src/services/context-handler.service.test.ts +501 -0
  148. package/src/services/context-handler.service.ts +372 -0
  149. package/src/services/environment.service.commands-prompt.test.ts +167 -0
  150. package/src/services/environment.service.ts +656 -0
  151. package/src/services/output.service.test.ts +92 -0
  152. package/src/services/output.service.ts +122 -0
  153. package/src/services/quiet-mode.service.test.ts +114 -0
  154. package/src/services/quiet-mode.service.ts +81 -0
  155. package/src/services/stream-output.service.test.ts +214 -0
  156. package/src/services/stream-output.service.ts +323 -0
  157. package/src/util/which.test.ts +101 -0
  158. package/src/util/which.ts +55 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @fileoverview Sessions Command Entry
3
+ *
4
+ * 命令入口:roy sessions [action]
5
+ *
6
+ * 注意:--quiet / -q 选项现在由 cli.ts 的全局 middleware 统一处理
7
+ */
8
+
9
+ import { CommandModule } from "yargs";
10
+ import { ListCommand } from "./list";
11
+ import { GetCommand } from "./get";
12
+ import { NewCommand } from "./new";
13
+ import { RenameCommand } from "./rename";
14
+ import { DeleteCommand } from "./delete";
15
+ import { MessagesCommand } from "./messages";
16
+ import { CompactCommand } from "./compact";
17
+ import { CheckpointsCommand } from "./checkpoints";
18
+ import { ActiveCommand } from "./active";
19
+ import { AddMessageCommand } from "./add-message";
20
+ import { MockCommand } from "./mock";
21
+ import { GrepCommand } from "./grep";
22
+
23
+ /**
24
+ * Sessions Command
25
+ *
26
+ * 会话管理命令,提供以下子命令:
27
+ * - list: 列出所有会话
28
+ * - get: 获取会话详情
29
+ * - new: 创建新会话
30
+ * - rename: 更新会话标题
31
+ * - delete: 删除会话(支持批量删除、全量删除、按时间删除)
32
+ * - messages: 查看消息
33
+ * - grep: 全文搜索消息
34
+ * - compact: 压缩会话 (new)
35
+ * - checkpoints: 查看检查点 (new)
36
+ * - active: 活跃会话管理
37
+ * - add-message: 添加单条消息
38
+ * - mock: 插入模拟消息(用于测试)
39
+ *
40
+ * 删除命令使用示例:
41
+ * roy sessions delete <id> # 删除单个会话
42
+ * roy sessions delete <id1> <id2> <id3> # 批量删除
43
+ * roy sessions delete --all # 删除所有会话
44
+ * roy sessions delete --all --keep-active # 删除所有非活跃会话
45
+ * roy sessions delete --older-than 30 # 删除30天前的会话
46
+ * roy sessions delete --dry-run # 预览模式
47
+ *
48
+ * 搜索命令使用示例:
49
+ * roy sessions grep K8s deployment error # 多关键字搜索
50
+ * roy sessions grep "exact phrase" # 精确短语
51
+ * roy sessions grep --session <id> query # 搜索指定会话
52
+ * roy sessions grep --limit 10 query # 限制结果数
53
+ * roy sessions grep --json query # JSON 输出
54
+ */
55
+ export const SessionsCommand: CommandModule = {
56
+ command: "sessions",
57
+ describe: `会话管理 - 列出、创建、删除和管理会话
58
+
59
+ 删除命令使用示例:
60
+ roy sessions delete <id> # 删除单个会话
61
+ roy sessions delete <id1> <id2> <id3> # 批量删除
62
+ roy sessions delete --all # 删除所有会话
63
+ roy sessions delete --all --keep-active # 删除所有非活跃会话
64
+ roy sessions delete --older-than 30 # 删除30天前的会话
65
+ roy sessions delete --dry-run # 预览模式
66
+
67
+ 搜索命令使用示例:
68
+ roy sessions grep K8s deployment error # 多关键字搜索
69
+ roy sessions grep "exact phrase" # 精确短语
70
+ roy sessions grep --session <id> query # 搜索指定会话
71
+ roy sessions grep --limit 10 query # 限制结果数
72
+ roy sessions grep --json query # JSON 输出`,
73
+ builder: (yargs) => {
74
+ // 注意:--quiet / -q 选项现在由 cli.ts 的全局 middleware 统一处理
75
+ return yargs
76
+ .command(ListCommand)
77
+ .command(GetCommand)
78
+ .command(NewCommand)
79
+ .command(RenameCommand)
80
+ .command(DeleteCommand)
81
+ .command(MessagesCommand)
82
+ .command(GrepCommand)
83
+ .command(CompactCommand)
84
+ .command(CheckpointsCommand)
85
+ .command(ActiveCommand)
86
+ .command(AddMessageCommand)
87
+ .command(MockCommand)
88
+ .demandCommand()
89
+ .help();
90
+ },
91
+ handler: () => {
92
+ // Default handler - shouldn't be reached if demandCommand is true
93
+ console.log("Use 'roy sessions --help' for usage information");
94
+ },
95
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @fileoverview Sessions List Command
3
+ *
4
+ * 命令:roy sessions list
5
+ */
6
+
7
+ import { CommandModule } from "yargs";
8
+ import { EnvironmentService } from "../../services/environment.service";
9
+ import { OutputService } from "../../services/output.service";
10
+ import { CliQuietModeService } from "../../services/quiet-mode.service";
11
+ import chalk from "chalk";
12
+ import type {
13
+ SessionComponent,
14
+ Session,
15
+ ListSessionsOptions,
16
+ } from "@ai-setting/roy-agent-core";
17
+
18
+ // Inline type for WorkflowSessionMetadata to avoid import issue
19
+ interface WorkflowSessionMetadata {
20
+ type: 'workflow';
21
+ workflowId?: string;
22
+ workflowName: string;
23
+ status: 'running' | 'paused' | 'completed' | 'failed';
24
+ }
25
+
26
+ interface ListOptions {
27
+ limit?: number;
28
+ offset?: number;
29
+ sort?: string;
30
+ order?: string;
31
+ json?: boolean;
32
+ quiet?: boolean;
33
+ id?: boolean;
34
+ config?: string;
35
+ type?: string;
36
+ status?: string;
37
+ }
38
+
39
+ export const ListCommand: CommandModule<object, ListOptions> = {
40
+ command: "list [options]",
41
+ aliases: ["ls"],
42
+ describe: "列出所有会话",
43
+
44
+ builder: (yargs) =>
45
+ yargs
46
+ .option("limit", { alias: "n", type: "number", default: 20 })
47
+ .option("offset", { type: "number", default: 0 })
48
+ .option("sort", { type: "string", default: "updatedAt" })
49
+ .option("order", { type: "string", default: "desc" })
50
+ .option("json", { alias: "j", type: "boolean", default: false })
51
+ .option("quiet", { alias: "q", type: "boolean", default: false })
52
+ .option("id", { alias: "i", type: "boolean", default: false, description: "显示 Session ID" })
53
+ .option("type", { alias: "t", type: "string", description: "按 session 类型过滤 (如 workflow)" })
54
+ .option("status", { alias: "s", type: "string", description: "按状态过滤 (如 running, paused)" }),
55
+
56
+ async handler(args) {
57
+ const isQuiet = args.quiet === true;
58
+ if (isQuiet) {
59
+ CliQuietModeService.getInstance().setQuiet(true);
60
+ }
61
+
62
+ const output = new OutputService();
63
+ output.configure({ quiet: isQuiet });
64
+ const envService = new EnvironmentService(output);
65
+
66
+ try {
67
+ await envService.create({ configPath: args.config });
68
+ const env = envService.getEnvironment();
69
+ if (!env) {
70
+ output.error("Failed to create environment");
71
+ process.exit(1);
72
+ }
73
+ const sessionComponent = env.getComponent("session") as SessionComponent;
74
+
75
+ if (!sessionComponent) {
76
+ output.error("SessionComponent not available");
77
+ process.exit(1);
78
+ }
79
+
80
+ const listOptions: ListSessionsOptions = {
81
+ sort: { field: args.sort as any, order: args.order as any },
82
+ offset: args.offset,
83
+ limit: args.limit,
84
+ };
85
+
86
+ // Add metadata filter if type or status is specified
87
+ if (args.type || args.status) {
88
+ const metadataFilter: Record<string, unknown> = {};
89
+ if (args.type) {
90
+ metadataFilter.type = args.type;
91
+ }
92
+ if (args.status) {
93
+ metadataFilter.status = args.status;
94
+ }
95
+ listOptions.filter = {
96
+ metadata: metadataFilter,
97
+ };
98
+ }
99
+
100
+ const sessions = await sessionComponent.list(listOptions);
101
+ const totalCount = await sessionComponent.getCount();
102
+ const activeSessionId = sessionComponent.getActiveSessionId();
103
+
104
+ if (args.json) {
105
+ output.json({
106
+ sessions: sessions.map((s: Session) => {
107
+ const workflowMeta = s.metadata?.type === 'workflow'
108
+ ? s.metadata as unknown as WorkflowSessionMetadata
109
+ : null;
110
+ return {
111
+ id: s.id,
112
+ title: s.title,
113
+ directory: s.directory,
114
+ messageCount: s.messageCount,
115
+ type: workflowMeta?.type || 'chat',
116
+ status: workflowMeta?.status,
117
+ workflowName: workflowMeta?.workflowName,
118
+ hasCheckpoint: !!s.metadata?.checkpoints?.checkpoints?.length,
119
+ lastCheckpointAt: s.metadata?.checkpoints?.latestCheckpointId,
120
+ createdAt: new Date(s.createdAt).toISOString(),
121
+ updatedAt: new Date(s.updatedAt).toISOString(),
122
+ isActive: s.id === activeSessionId,
123
+ };
124
+ }),
125
+ total: sessions.length,
126
+ totalCount: totalCount,
127
+ });
128
+ } else if (args.quiet || args.id) {
129
+ // 简洁输出:只显示 ID
130
+ sessions.forEach((s: Session) => output.log(s.id));
131
+ } else {
132
+ // 表格输出
133
+ const hasTypeFilter = !!args.type;
134
+ const hasStatusFilter = !!args.status;
135
+
136
+ const header = [
137
+ chalk.bold("#"),
138
+ chalk.bold("Title"),
139
+ chalk.bold("ID"),
140
+ hasTypeFilter || hasStatusFilter ? chalk.bold("Status") : null,
141
+ chalk.bold("Msgs"),
142
+ chalk.bold("CP"),
143
+ ].filter(Boolean).join(" │ ");
144
+
145
+ const rows = sessions.map((s: Session, i: number) => {
146
+ const marker = s.id === activeSessionId ? chalk.green("▶") : " ";
147
+ const hasCp = (s.metadata?.checkpoints?.checkpoints?.length ?? 0) > 0;
148
+ const title = s.title.length > 20 ? s.title.slice(0, 17) + "..." : s.title;
149
+ // 显示完整的 Session ID
150
+ const idStr = s.id;
151
+
152
+ // Get workflow metadata if present
153
+ const workflowMeta = s.metadata?.type === 'workflow'
154
+ ? s.metadata as unknown as WorkflowSessionMetadata
155
+ : null;
156
+ const typeStr = workflowMeta?.type || '';
157
+ const statusStr = workflowMeta?.status || '';
158
+
159
+ // Format status for display
160
+ let statusDisplay = '';
161
+ if (hasTypeFilter || hasStatusFilter) {
162
+ if (workflowMeta) {
163
+ statusDisplay = workflowMeta.status === 'running' ? chalk.green('running')
164
+ : workflowMeta.status === 'paused' ? chalk.yellow('paused')
165
+ : workflowMeta.status === 'completed' ? chalk.gray('completed')
166
+ : workflowMeta.status === 'failed' ? chalk.red('failed')
167
+ : statusStr;
168
+ }
169
+ }
170
+
171
+ const cells = [
172
+ marker + " " + (args.offset! + i + 1),
173
+ title,
174
+ chalk.gray(idStr),
175
+ ];
176
+
177
+ if (hasTypeFilter || hasStatusFilter) {
178
+ cells.push(statusDisplay || chalk.gray('-'));
179
+ }
180
+
181
+ cells.push(
182
+ s.messageCount.toString(),
183
+ hasCp ? chalk.green("✓") : chalk.gray("-"),
184
+ );
185
+
186
+ return cells.join(" │ ");
187
+ });
188
+
189
+ const showingInfo = totalCount > sessions.length
190
+ ? `Showing ${sessions.length} of ${totalCount} sessions`
191
+ : `${totalCount} sessions`;
192
+
193
+ output.log([
194
+ `┌─ Sessions ${"─".repeat(50)}┐`,
195
+ `│${header}│`,
196
+ "├" + "─".repeat(header.length + 2) + "┤",
197
+ ...rows.map((r: string) => `│${r}│`),
198
+ "└" + "─".repeat(header.length + 2) + "┘",
199
+ "",
200
+ chalk.gray(showingInfo),
201
+ ].join("\n"));
202
+ }
203
+ } catch (error) {
204
+ output.error(`Failed to list sessions: ${error instanceof Error ? error.message : String(error)}`);
205
+ process.exit(1);
206
+ } finally {
207
+ await envService.dispose();
208
+ }
209
+ },
210
+ };
@@ -0,0 +1,333 @@
1
+ /**
2
+ * @fileoverview Messages Command Tests
3
+ *
4
+ * TDD: 测试 sessions messages 命令的分页和总数功能
5
+ */
6
+
7
+ import { describe, test, expect, vi, beforeEach } from "bun:test";
8
+ import type { SessionMessage } from "@ai-setting/roy-agent-core";
9
+
10
+ describe("MessagesCommand MessagesOptions", () => {
11
+ test("should have required offset parameter", () => {
12
+ // Given: 期望 offset 是必填参数
13
+ const options = {
14
+ sessionId: "test-session",
15
+ offset: 10,
16
+ limit: 20,
17
+ };
18
+
19
+ // Then: offset 和 limit 应该是必填的
20
+ expect(options.offset).toBeDefined();
21
+ expect(options.limit).toBeDefined();
22
+ });
23
+
24
+ test("should have required limit parameter", () => {
25
+ const options = {
26
+ sessionId: "test-session",
27
+ offset: 0,
28
+ limit: 50,
29
+ };
30
+
31
+ expect(options.offset).toBeDefined();
32
+ expect(options.limit).toBeDefined();
33
+ });
34
+ });
35
+
36
+ describe("MessagesCommand Pagination Logic", () => {
37
+ // Mock SessionComponent
38
+ let mockSessionComponent: {
39
+ get: ReturnType<typeof vi.fn>;
40
+ getMessages: ReturnType<typeof vi.fn>;
41
+ getMessageCount: ReturnType<typeof vi.fn>;
42
+ };
43
+
44
+ beforeEach(() => {
45
+ vi.clearAllMocks();
46
+
47
+ mockSessionComponent = {
48
+ get: vi.fn(),
49
+ getMessages: vi.fn(),
50
+ getMessageCount: vi.fn(),
51
+ };
52
+
53
+ // Setup default mocks
54
+ mockSessionComponent.get.mockResolvedValue({
55
+ id: "test-session",
56
+ title: "Test Session",
57
+ });
58
+
59
+ mockSessionComponent.getMessageCount.mockResolvedValue(100);
60
+ mockSessionComponent.getMessages.mockResolvedValue([
61
+ {
62
+ id: "msg-1",
63
+ sessionID: "test-session",
64
+ role: "user",
65
+ content: "Hello",
66
+ timestamp: Date.now(),
67
+ },
68
+ ]);
69
+ });
70
+
71
+ test("should call getMessages with offset and limit", async () => {
72
+ // Given: offset=10, limit=20
73
+ const offset = 10;
74
+ const limit = 20;
75
+
76
+ // When: 调用 getMessages
77
+ const messages = await mockSessionComponent.getMessages("test-session", {
78
+ offset,
79
+ limit,
80
+ });
81
+
82
+ // Then: 应该传入正确的 offset 和 limit
83
+ expect(mockSessionComponent.getMessages).toHaveBeenCalledWith(
84
+ "test-session",
85
+ { offset: 10, limit: 20 }
86
+ );
87
+ expect(messages).toHaveLength(1);
88
+ });
89
+
90
+ test("should call getMessageCount to get total", async () => {
91
+ // Given
92
+ const sessionId = "test-session";
93
+
94
+ // When: 获取消息总数
95
+ const totalCount = await mockSessionComponent.getMessageCount(sessionId);
96
+
97
+ // Then: 应该返回正确的总数
98
+ expect(mockSessionComponent.getMessageCount).toHaveBeenCalledWith(sessionId);
99
+ expect(totalCount).toBe(100);
100
+ });
101
+
102
+ test("should return messages with pagination info", async () => {
103
+ // Given
104
+ const sessionId = "test-session";
105
+ const offset = 0;
106
+ const limit = 10;
107
+
108
+ // 模拟分页数据
109
+ const mockMessages: SessionMessage[] = Array.from({ length: 10 }, (_, i) => ({
110
+ id: `msg-${i}`,
111
+ sessionID: sessionId,
112
+ role: i % 2 === 0 ? "user" : "assistant",
113
+ content: `Message ${i}`,
114
+ timestamp: Date.now() + i,
115
+ }));
116
+
117
+ mockSessionComponent.getMessages.mockResolvedValue(mockMessages);
118
+ mockSessionComponent.getMessageCount.mockResolvedValue(100);
119
+
120
+ // When
121
+ const [messages, totalCount] = await Promise.all([
122
+ mockSessionComponent.getMessages(sessionId, { offset, limit }),
123
+ mockSessionComponent.getMessageCount(sessionId),
124
+ ]);
125
+
126
+ // Then
127
+ expect(messages).toHaveLength(10);
128
+ expect(totalCount).toBe(100);
129
+ });
130
+
131
+ test("should handle edge case: offset equals total count", async () => {
132
+ // Given: offset 等于总数(最后一页之后)
133
+ const sessionId = "test-session";
134
+ mockSessionComponent.getMessages.mockResolvedValue([]);
135
+ mockSessionComponent.getMessageCount.mockResolvedValue(50);
136
+
137
+ // When
138
+ const [messages, totalCount] = await Promise.all([
139
+ mockSessionComponent.getMessages(sessionId, { offset: 50, limit: 10 }),
140
+ mockSessionComponent.getMessageCount(sessionId),
141
+ ]);
142
+
143
+ // Then
144
+ expect(messages).toHaveLength(0);
145
+ expect(totalCount).toBe(50);
146
+ });
147
+ });
148
+
149
+ describe("MessagesCommand JSON Output", () => {
150
+ test("should include total count in JSON output", () => {
151
+ // Given: JSON 响应应该包含总数
152
+ const jsonResponse = {
153
+ sessionId: "test-session",
154
+ messages: [
155
+ {
156
+ id: "msg-1",
157
+ role: "user",
158
+ content: "Hello",
159
+ timestamp: new Date().toISOString(),
160
+ },
161
+ ],
162
+ total: 100, // 消息总数
163
+ pagination: {
164
+ offset: 0,
165
+ limit: 10,
166
+ },
167
+ };
168
+
169
+ // Then: 应该包含 total 字段
170
+ expect(jsonResponse).toHaveProperty("total");
171
+ expect(jsonResponse.total).toBe(100);
172
+ });
173
+
174
+ test("should include pagination info in JSON output", () => {
175
+ // Given
176
+ const jsonResponse = {
177
+ sessionId: "test-session",
178
+ messages: [],
179
+ total: 100,
180
+ pagination: {
181
+ offset: 10,
182
+ limit: 20,
183
+ hasMore: true, // 是否有更多消息
184
+ },
185
+ };
186
+
187
+ // Then
188
+ expect(jsonResponse.pagination).toBeDefined();
189
+ expect(jsonResponse.pagination.offset).toBe(10);
190
+ expect(jsonResponse.pagination.limit).toBe(20);
191
+ });
192
+ });
193
+
194
+ describe("MessagesCommand CLI Options", () => {
195
+ test("offset should be required in CLI builder", () => {
196
+ // Given: CLI 参数定义
197
+ const expectedOptions = {
198
+ offset: { alias: "o", type: "number", demandOption: true },
199
+ limit: { alias: "n", type: "number", demandOption: true },
200
+ };
201
+
202
+ // Then: offset 和 limit 应该是必填的 (demandOption: true)
203
+ expect(expectedOptions.offset.demandOption).toBe(true);
204
+ expect(expectedOptions.limit.demandOption).toBe(true);
205
+ });
206
+
207
+ test("should have default values for optional parameters", () => {
208
+ // Note: 虽然 offset 和 limit 是必填的,但 reverse 和 json 有默认值
209
+ const cliOptions = {
210
+ offset: { alias: "o", type: "number", demandOption: true },
211
+ limit: { alias: "n", type: "number", demandOption: true },
212
+ reverse: { alias: "r", type: "boolean", default: false },
213
+ json: { alias: "j", type: "boolean", default: false },
214
+ };
215
+
216
+ expect(cliOptions.reverse.default).toBe(false);
217
+ expect(cliOptions.json.default).toBe(false);
218
+ });
219
+ });
220
+
221
+ describe("MessagesCommand Format Message Content", () => {
222
+ test("should handle tool-call with undefined arguments without error", () => {
223
+ // Given: tool-call part 的 arguments 是 undefined
224
+ const mockMessage = {
225
+ id: "msg-1",
226
+ role: "assistant",
227
+ content: "",
228
+ timestamp: Date.now(),
229
+ parts: [
230
+ { type: "tool-call", toolCallId: "call_1", toolName: "bash", arguments: undefined, state: "pending" },
231
+ ],
232
+ };
233
+
234
+ // Then: 应该能安全处理 undefined arguments
235
+ const formatMessageContent = (m: any): string[] => {
236
+ const lines: string[] = [];
237
+ if (m.parts && m.parts.length > 0) {
238
+ for (const part of m.parts) {
239
+ if (part.type === "tool-call") {
240
+ lines.push(`[Tool Call] ${part.toolName}`);
241
+ // 修复后的代码逻辑
242
+ const argsStr = typeof part.arguments === 'string'
243
+ ? part.arguments
244
+ : JSON.stringify(part.arguments ?? null, null, 2);
245
+ if (argsStr && argsStr.length > 200) {
246
+ lines.push(...argsStr.slice(0, 197).split("\n").map(() => "line"));
247
+ lines.push("...");
248
+ } else if (argsStr) {
249
+ lines.push(...argsStr.split("\n").map(() => "line"));
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return lines;
255
+ };
256
+
257
+ // Should not throw
258
+ expect(() => formatMessageContent(mockMessage)).not.toThrow();
259
+ const result = formatMessageContent(mockMessage);
260
+ expect(result).toContain("[Tool Call] bash");
261
+ });
262
+ });
263
+
264
+ describe("MessagesCommand getMessageCount Integration", () => {
265
+ let mockSessionComponent: {
266
+ getMessageCount: ReturnType<typeof vi.fn>;
267
+ getMessages: ReturnType<typeof vi.fn>;
268
+ };
269
+
270
+ beforeEach(() => {
271
+ vi.clearAllMocks();
272
+
273
+ mockSessionComponent = {
274
+ getMessageCount: vi.fn(),
275
+ getMessages: vi.fn(),
276
+ };
277
+ });
278
+
279
+ test("should use getMessageCount to get total messages", async () => {
280
+ // Given
281
+ mockSessionComponent.getMessageCount.mockResolvedValue(150);
282
+
283
+ // When
284
+ const total = await mockSessionComponent.getMessageCount("session-123");
285
+
286
+ // Then
287
+ expect(total).toBe(150);
288
+ expect(mockSessionComponent.getMessageCount).toHaveBeenCalledWith("session-123");
289
+ });
290
+
291
+ test("should calculate hasMore based on offset, limit and total", async () => {
292
+ // Given
293
+ const offset = 90;
294
+ const limit = 20;
295
+ const total = 100;
296
+
297
+ // When: 计算是否还有更多消息
298
+ // offset + limit < total 表示还有下一页
299
+ const hasMore = offset + limit < total;
300
+
301
+ // Then: offset=90, limit=20, total=100
302
+ // offset + limit = 110, 110 < 100 = false
303
+ expect(hasMore).toBe(false);
304
+ });
305
+
306
+ test("should correctly determine hasMore when at last page", async () => {
307
+ // Given
308
+ const offset = 90;
309
+ const limit = 10;
310
+ const total = 100;
311
+
312
+ // When
313
+ const hasMore = offset + limit < total;
314
+
315
+ // Then: offset=90, limit=10, total=100
316
+ // offset + limit = 100, 100 < 100 = false
317
+ expect(hasMore).toBe(false);
318
+ });
319
+
320
+ test("should correctly determine hasMore when more pages exist", async () => {
321
+ // Given
322
+ const offset = 0;
323
+ const limit = 10;
324
+ const total = 100;
325
+
326
+ // When
327
+ const hasMore = offset + limit < total;
328
+
329
+ // Then: offset=0, limit=10, total=100
330
+ // 0 + 10 = 10, 10 < 100 = true
331
+ expect(hasMore).toBe(true);
332
+ });
333
+ });