@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,185 @@
1
+ /**
2
+ * @fileoverview EventSource Status Command
3
+ *
4
+ * 查看事件源状态
5
+ */
6
+
7
+ import { CommandModule } from "yargs";
8
+ import { EnvironmentService } from "../../services/environment.service";
9
+ import { OutputService } from "../../services/output.service";
10
+ import chalk from "chalk";
11
+ import type { EventSourceComponentInterface, EventSourceStatus } from "@ai-setting/roy-agent-core";
12
+
13
+ export interface EventSourceStatusOptions {
14
+ id?: string;
15
+ config?: string;
16
+ json?: boolean;
17
+ }
18
+
19
+ const STATUS_COLORS: Record<EventSourceStatus, (text: string) => string> = {
20
+ created: chalk.gray,
21
+ starting: chalk.yellow,
22
+ running: chalk.green,
23
+ stopping: chalk.yellow,
24
+ stopped: chalk.gray,
25
+ error: chalk.red,
26
+ };
27
+
28
+ const STATUS_ICONS: Record<EventSourceStatus, string> = {
29
+ created: "○",
30
+ starting: "◐",
31
+ running: "●",
32
+ stopping: "◑",
33
+ stopped: "○",
34
+ error: "✗",
35
+ };
36
+
37
+ const STATUS_LABELS: Record<EventSourceStatus, string> = {
38
+ created: "已创建",
39
+ starting: "启动中",
40
+ running: "运行中",
41
+ stopping: "停止中",
42
+ stopped: "已停止",
43
+ error: "错误",
44
+ };
45
+
46
+ export const EventSourceStatusCommand: CommandModule<object, EventSourceStatusOptions> = {
47
+ command: "status [id]",
48
+ aliases: ["stat"],
49
+ describe: "查看事件源状态",
50
+
51
+ builder: (yargs) =>
52
+ yargs
53
+ .positional("id", {
54
+ describe: "事件源 ID(可选,查看所有或单个)",
55
+ type: "string",
56
+ })
57
+ .option("json", {
58
+ alias: "j",
59
+ describe: "JSON 格式输出",
60
+ type: "boolean",
61
+ default: false,
62
+ }),
63
+
64
+ async handler(args) {
65
+ const output = new OutputService();
66
+ const envService = new EnvironmentService(output);
67
+
68
+ try {
69
+ await envService.create({ configPath: args.config });
70
+ const env = envService.getEnvironment();
71
+ if (!env) {
72
+ output.error("Failed to create environment");
73
+ process.exit(1);
74
+ }
75
+ const esComponent = env.getComponent("event-source") as unknown as EventSourceComponentInterface | undefined;
76
+
77
+ if (!esComponent) {
78
+ output.error("EventSourceComponent not available");
79
+ process.exit(1);
80
+ }
81
+
82
+ // 如果指定了 ID,查看单个
83
+ if (args.id) {
84
+ const sources = esComponent.list();
85
+ const matchedSource = sources.find(
86
+ (s) => s.id === args.id || s.id.startsWith(args.id!)
87
+ );
88
+
89
+ if (!matchedSource) {
90
+ output.error(`事件源不存在: ${args.id}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ const status = esComponent.getStatus(matchedSource.id) || "unknown";
95
+ const statusColor = STATUS_COLORS[status as EventSourceStatus] || chalk.gray;
96
+
97
+ if (args.json) {
98
+ output.json({
99
+ id: matchedSource.id,
100
+ name: matchedSource.name,
101
+ type: matchedSource.type,
102
+ status,
103
+ enabled: matchedSource.enabled,
104
+ eventTypes: matchedSource.eventTypes,
105
+ config: {
106
+ command: matchedSource.command,
107
+ interval: matchedSource.interval,
108
+ url: matchedSource.url,
109
+ },
110
+ });
111
+ return;
112
+ }
113
+
114
+ output.log(chalk.bold("事件源详情"));
115
+ output.log("─".repeat(50));
116
+ output.log(` ID: ${chalk.gray(matchedSource.id)}`);
117
+ output.log(` Name: ${chalk.cyan(matchedSource.name)}`);
118
+ output.log(` Type: ${chalk.cyan(matchedSource.type)}`);
119
+ output.log(` Status: ${statusColor(`${STATUS_ICONS[status as EventSourceStatus]} ${STATUS_LABELS[status as EventSourceStatus]}`)}`);
120
+ output.log(` Enabled: ${matchedSource.enabled ? chalk.green("是") : chalk.gray("否")}`);
121
+
122
+ if (matchedSource.eventTypes?.length) {
123
+ output.log(` Events: ${chalk.gray(matchedSource.eventTypes.join(", "))}`);
124
+ }
125
+ if (matchedSource.command) {
126
+ output.log(` Command: ${chalk.gray(matchedSource.command)}`);
127
+ }
128
+ if (matchedSource.interval) {
129
+ output.log(` Interval: ${chalk.gray(`${matchedSource.interval}ms`)}`);
130
+ }
131
+ if (matchedSource.url) {
132
+ output.log(` URL: ${chalk.gray(matchedSource.url)}`);
133
+ }
134
+ output.log("");
135
+ return;
136
+ }
137
+
138
+ // 查看所有状态
139
+ const sources = esComponent.list();
140
+
141
+ if (sources.length === 0) {
142
+ output.log(chalk.yellow("没有配置的事件源"));
143
+ return;
144
+ }
145
+
146
+ if (args.json) {
147
+ const sourcesWithStatus = sources.map((s) => ({
148
+ id: s.id,
149
+ name: s.name,
150
+ type: s.type,
151
+ status: esComponent.getStatus(s.id) || "unknown",
152
+ enabled: s.enabled,
153
+ }));
154
+ output.json({ sources: sourcesWithStatus, count: sources.length });
155
+ return;
156
+ }
157
+
158
+ output.log(chalk.bold("事件源状态概览"));
159
+ output.log("─".repeat(60));
160
+
161
+ for (const source of sources) {
162
+ const status = esComponent.getStatus(source.id) || "unknown";
163
+ const statusColor = STATUS_COLORS[status as EventSourceStatus] || chalk.gray;
164
+ const icon = STATUS_ICONS[status as EventSourceStatus] || "?";
165
+ const label = STATUS_LABELS[status as EventSourceStatus] || status;
166
+
167
+ output.log("");
168
+ output.log(` ${chalk.cyan(source.name)} ${chalk.gray(`(${source.type})`)}`);
169
+ output.log(` └─ Status: ${statusColor(`${icon} ${label}`)}`);
170
+ output.log(` ID: ${chalk.gray(source.id.substring(0, 8))}...`);
171
+ }
172
+
173
+ output.log("");
174
+ const runningCount = sources.filter(
175
+ (s) => esComponent.getStatus(s.id) === "running"
176
+ ).length;
177
+ output.log(
178
+ chalk.green(`✅ ${runningCount}/${sources.length} 运行中`)
179
+ );
180
+
181
+ } finally {
182
+ await envService.dispose();
183
+ }
184
+ },
185
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @fileoverview EventSource Stop Command
3
+ *
4
+ * 停止事件源
5
+ */
6
+
7
+ import { CommandModule } from "yargs";
8
+ import { EnvironmentService } from "../../services/environment.service";
9
+ import { OutputService } from "../../services/output.service";
10
+ import chalk from "chalk";
11
+ import type { EventSourceComponentInterface } from "@ai-setting/roy-agent-core";
12
+
13
+ export interface EventSourceStopOptions {
14
+ id: string;
15
+ config?: string;
16
+ force?: boolean;
17
+ }
18
+
19
+ export const EventSourceStopCommand: CommandModule<object, EventSourceStopOptions> = {
20
+ command: "stop <id>",
21
+ aliases: ["kill"],
22
+ describe: "停止指定的事件源",
23
+
24
+ builder: (yargs) =>
25
+ yargs
26
+ .positional("id", {
27
+ describe: "事件源 ID(支持前缀匹配)",
28
+ type: "string",
29
+ demandOption: true,
30
+ })
31
+ .option("force", {
32
+ alias: "f",
33
+ describe: "强制停止",
34
+ type: "boolean",
35
+ default: false,
36
+ }),
37
+
38
+ async handler(args) {
39
+ const output = new OutputService();
40
+ const envService = new EnvironmentService(output);
41
+
42
+ try {
43
+ await envService.create({ configPath: args.config });
44
+ const env = envService.getEnvironment();
45
+ if (!env) {
46
+ output.error("Failed to create environment");
47
+ process.exit(1);
48
+ }
49
+ const esComponent = env.getComponent("event-source") as unknown as EventSourceComponentInterface | undefined;
50
+
51
+ if (!esComponent) {
52
+ output.error("EventSourceComponent not available");
53
+ process.exit(1);
54
+ }
55
+
56
+ // 查找事件源(支持前缀匹配)
57
+ const sources = esComponent.list();
58
+ const matchedSource = sources.find(
59
+ (s) => s.id === args.id || s.id.startsWith(args.id)
60
+ );
61
+
62
+ if (!matchedSource) {
63
+ output.error(`事件源不存在: ${args.id}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ // 检查状态
68
+ const status = esComponent.getStatus(matchedSource.id);
69
+ if (status !== "running") {
70
+ output.log(chalk.yellow(`事件源未运行: ${matchedSource.name} (${status || "unknown"})`));
71
+ return;
72
+ }
73
+
74
+ // 停止
75
+ output.info(`正在停止事件源 '${matchedSource.name}'...`);
76
+
77
+ try {
78
+ await esComponent.stopSource(matchedSource.id);
79
+ output.success(chalk.green(`事件源已停止: ${matchedSource.name}`));
80
+ } catch (error) {
81
+ output.error(`停止失败: ${error}`);
82
+ process.exit(1);
83
+ }
84
+
85
+ } finally {
86
+ await envService.dispose();
87
+ }
88
+ },
89
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview CLI Commands Index
3
+ *
4
+ * 导出所有内置命令,供插件复用
5
+ */
6
+
7
+ export { ActCommand } from "./act";
8
+ export type { ActOptions } from "./act";
9
+
10
+ export { InteractiveCommand } from "./interactive";
11
+
12
+ export { SessionsCommand } from "./sessions";
13
+
14
+ export { TasksCommand } from "./tasks";
15
+
16
+ export { CommandsCommand } from "./commands";
17
+
18
+ export { ToolsCommand } from "./tools";
19
+
20
+ export { EventSourceCommand } from "./eventsource";
21
+
22
+ export { WorkflowCommand } from "./workflow";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @fileoverview InputHandler - TDD Tests
3
+ *
4
+ * 核心设计:
5
+ * - 纯粹 buffer 管理
6
+ * - 只负责 pushLine, getBuffer, reset, getPrompt
7
+ */
8
+
9
+ import { describe, test, expect } from "bun:test";
10
+ import { InputHandler } from "./input-handler";
11
+
12
+ describe("InputHandler", () => {
13
+
14
+ describe("pushLine - 添加输入", () => {
15
+
16
+ test("添加单行内容", () => {
17
+ const handler = new InputHandler();
18
+ handler.pushLine("hello");
19
+ expect(handler.getBuffer()).toBe("hello");
20
+ });
21
+
22
+ test("添加多行内容", () => {
23
+ const handler = new InputHandler();
24
+ handler.pushLine("line1");
25
+ handler.pushLine("line2");
26
+ expect(handler.getBuffer()).toBe("line1\nline2");
27
+ });
28
+
29
+ test("添加空行", () => {
30
+ const handler = new InputHandler();
31
+ handler.pushLine("hello");
32
+ handler.pushLine("");
33
+ expect(handler.getBuffer()).toBe("hello\n");
34
+ });
35
+
36
+ test("支持空白行", () => {
37
+ const handler = new InputHandler();
38
+ handler.pushLine("line1");
39
+ handler.pushLine("");
40
+ handler.pushLine("");
41
+ handler.pushLine("line2");
42
+ expect(handler.getBuffer()).toBe("line1\n\n\nline2");
43
+ });
44
+ });
45
+
46
+ describe("reset - 重置", () => {
47
+
48
+ test("重置后 buffer 为空", () => {
49
+ const handler = new InputHandler();
50
+ handler.pushLine("hello");
51
+ handler.reset();
52
+ expect(handler.getBuffer()).toBe("");
53
+ });
54
+ });
55
+
56
+ describe("getPrompt - 获取提示符", () => {
57
+
58
+ test("初始状态返回用户提示符", () => {
59
+ const handler = new InputHandler();
60
+ expect(handler.getPrompt()).toBe("❯ ");
61
+ });
62
+
63
+ test("有多行输入时返回继续提示符", () => {
64
+ const handler = new InputHandler();
65
+ handler.pushLine("hello");
66
+ expect(handler.getPrompt()).toBe("... ");
67
+ });
68
+
69
+ test("reset 后恢复用户提示符", () => {
70
+ const handler = new InputHandler();
71
+ handler.pushLine("hello");
72
+ handler.reset();
73
+ expect(handler.getPrompt()).toBe("❯ ");
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview InputHandler - 纯粹 buffer 管理
3
+ *
4
+ * 只负责累积输入,不处理结束检测
5
+ */
6
+
7
+ export class InputHandler {
8
+ private buffer: string[] = [];
9
+
10
+ // 提示符
11
+ private static readonly USER_PROMPT = "❯ ";
12
+ private static readonly CONTINUATION_PROMPT = "... ";
13
+
14
+ /**
15
+ * 添加一行输入
16
+ */
17
+ pushLine(line: string): void {
18
+ this.buffer.push(line);
19
+ }
20
+
21
+ /**
22
+ * 获取当前 buffer 内容
23
+ */
24
+ getBuffer(): string {
25
+ return this.buffer.join("\n");
26
+ }
27
+
28
+ /**
29
+ * 重置 buffer
30
+ */
31
+ reset(): void {
32
+ this.buffer = [];
33
+ }
34
+
35
+ /**
36
+ * 获取提示符
37
+ */
38
+ getPrompt(): string {
39
+ return this.buffer.length > 0
40
+ ? InputHandler.CONTINUATION_PROMPT
41
+ : InputHandler.USER_PROMPT;
42
+ }
43
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @fileoverview Interactive Esc Key Handler Tests
3
+ *
4
+ * TDD: 测试 Interactive 模式的 Esc 按键中断功能
5
+ *
6
+ * 场景 1: 正在执行时按 Esc - 中断 LLM 调用 + 停止流式输出 + 恢复 prompt
7
+ * 场景 2: 空闲时按 Esc - 不做任何操作
8
+ */
9
+
10
+ import { describe, test, expect, vi, beforeEach } from "bun:test";
11
+ import { REPL } from "./interactive";
12
+ import { COLORS, abortStream, resetStreamAbort, streamAbortSignal } from "../services/stream-output.service";
13
+
14
+ // ============================================================================
15
+ // Test Suite
16
+ // ============================================================================
17
+
18
+ describe("REPL.handleEscKey - Esc 按键中断流式输出", () => {
19
+ let mockExecute: ReturnType<typeof vi.fn>;
20
+ let mockStatus: ReturnType<typeof vi.fn>;
21
+ let mockShutdown: ReturnType<typeof vi.fn>;
22
+ let mockAbort: ReturnType<typeof vi.fn>;
23
+ let mockEnv: any;
24
+
25
+ beforeEach(() => {
26
+ // 重置 abort 信号
27
+ resetStreamAbort();
28
+
29
+ // 创建 mock 函数
30
+ mockExecute = vi.fn().mockResolvedValue("done");
31
+ mockStatus = vi.fn().mockReturnValue({
32
+ sessionId: "test",
33
+ sessionTitle: "Test",
34
+ messageCount: 0,
35
+ tokenCount: 0,
36
+ });
37
+ mockShutdown = vi.fn().mockResolvedValue(undefined);
38
+ mockAbort = vi.fn();
39
+
40
+ // Mock environment with agent component
41
+ mockEnv = {
42
+ getComponent: vi.fn().mockImplementation((name: string) => {
43
+ if (name === "agent") {
44
+ return { abort: mockAbort };
45
+ }
46
+ return undefined;
47
+ }),
48
+ };
49
+ });
50
+
51
+ test("handleEscKey() 方法应该存在", () => {
52
+ // Arrange
53
+ const repl = new REPL(
54
+ {
55
+ sessionId: "test",
56
+ sessionTitle: "Test",
57
+ onExecute: mockExecute,
58
+ onStatus: mockStatus,
59
+ onSwitchSession: vi.fn(),
60
+ onCompact: vi.fn(),
61
+ onShutdown: mockShutdown,
62
+ },
63
+ mockEnv
64
+ );
65
+
66
+ // Assert - handleEscKey 应该是 REPL 的一个方法
67
+ expect(typeof repl.handleEscKey).toBe("function");
68
+ });
69
+
70
+ test("正在执行时按 Esc - 应该调用 AgentComponent.abort", () => {
71
+ // Arrange
72
+ const repl = new REPL(
73
+ {
74
+ sessionId: "test",
75
+ sessionTitle: "Test",
76
+ onExecute: mockExecute,
77
+ onStatus: mockStatus,
78
+ onSwitchSession: vi.fn(),
79
+ onCompact: vi.fn(),
80
+ onShutdown: mockShutdown,
81
+ },
82
+ mockEnv
83
+ );
84
+
85
+ // 模拟正在执行状态
86
+ // 通过私有属性访问(测试用途)
87
+ (repl as any).isExecuting = true;
88
+
89
+ // Act
90
+ repl.handleEscKey();
91
+
92
+ // Assert - AgentComponent.abort 应该被调用
93
+ expect(mockAbort).toHaveBeenCalledWith("default");
94
+ expect(mockAbort).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ test("正在执行时按 Esc - 应该调用 abortStream", () => {
98
+ // Arrange
99
+ const repl = new REPL(
100
+ {
101
+ sessionId: "test",
102
+ sessionTitle: "Test",
103
+ onExecute: mockExecute,
104
+ onStatus: mockStatus,
105
+ onSwitchSession: vi.fn(),
106
+ onCompact: vi.fn(),
107
+ onShutdown: mockShutdown,
108
+ },
109
+ mockEnv
110
+ );
111
+
112
+ // 模拟正在执行状态
113
+ (repl as any).isExecuting = true;
114
+
115
+ // 重置确认
116
+ resetStreamAbort();
117
+ expect(streamAbortSignal.aborted).toBe(false);
118
+
119
+ // Act
120
+ repl.handleEscKey();
121
+
122
+ // Assert - abortStream 应该被调用,streamAbortSignal.aborted 应该为 true
123
+ expect(streamAbortSignal.aborted).toBe(true);
124
+ });
125
+
126
+ test("正在执行时按 Esc - 应该重置 isExecuting 状态为 false", () => {
127
+ // Arrange
128
+ const repl = new REPL(
129
+ {
130
+ sessionId: "test",
131
+ sessionTitle: "Test",
132
+ onExecute: mockExecute,
133
+ onStatus: mockStatus,
134
+ onSwitchSession: vi.fn(),
135
+ onCompact: vi.fn(),
136
+ onShutdown: mockShutdown,
137
+ },
138
+ mockEnv
139
+ );
140
+
141
+ // 模拟正在执行状态
142
+ (repl as any).isExecuting = true;
143
+ expect((repl as any).isExecuting).toBe(true);
144
+
145
+ // Act
146
+ repl.handleEscKey();
147
+
148
+ // Assert - isExecuting 应该被重置为 false
149
+ expect((repl as any).isExecuting).toBe(false);
150
+ });
151
+
152
+ test("空闲时按 Esc - 不应该调用 AgentComponent.abort", () => {
153
+ // Arrange
154
+ const repl = new REPL(
155
+ {
156
+ sessionId: "test",
157
+ sessionTitle: "Test",
158
+ onExecute: mockExecute,
159
+ onStatus: mockStatus,
160
+ onSwitchSession: vi.fn(),
161
+ onCompact: vi.fn(),
162
+ onShutdown: mockShutdown,
163
+ },
164
+ mockEnv
165
+ );
166
+
167
+ // 确保处于空闲状态
168
+ (repl as any).isExecuting = false;
169
+
170
+ // Act
171
+ repl.handleEscKey();
172
+
173
+ // Assert - AgentComponent.abort 不应该被调用
174
+ expect(mockAbort).not.toHaveBeenCalled();
175
+ });
176
+
177
+ test("空闲时按 Esc - 不应该调用 abortStream", () => {
178
+ // Arrange
179
+ const repl = new REPL(
180
+ {
181
+ sessionId: "test",
182
+ sessionTitle: "Test",
183
+ onExecute: mockExecute,
184
+ onStatus: mockStatus,
185
+ onSwitchSession: vi.fn(),
186
+ onCompact: vi.fn(),
187
+ onShutdown: mockShutdown,
188
+ },
189
+ mockEnv
190
+ );
191
+
192
+ // 确保处于空闲状态
193
+ (repl as any).isExecuting = false;
194
+
195
+ // 重置确认
196
+ resetStreamAbort();
197
+
198
+ // Act
199
+ repl.handleEscKey();
200
+
201
+ // Assert - abortStream 不应该被调用
202
+ expect(streamAbortSignal.aborted).toBe(false);
203
+ });
204
+
205
+ test("没有 env 时按 Esc - 应该仍然调用 abortStream", () => {
206
+ // Arrange - 不传入 env
207
+ const repl = new REPL(
208
+ {
209
+ sessionId: "test",
210
+ sessionTitle: "Test",
211
+ onExecute: mockExecute,
212
+ onStatus: mockStatus,
213
+ onSwitchSession: vi.fn(),
214
+ onCompact: vi.fn(),
215
+ onShutdown: mockShutdown,
216
+ },
217
+ undefined // 不传入 env
218
+ );
219
+
220
+ // 模拟正在执行状态
221
+ (repl as any).isExecuting = true;
222
+ resetStreamAbort();
223
+
224
+ // Act
225
+ repl.handleEscKey();
226
+
227
+ // Assert - 即使没有 env,abortStream 也应该被调用
228
+ expect(streamAbortSignal.aborted).toBe(true);
229
+ });
230
+ });
231
+
232
+ describe("Esc 与 Ctrl+C 的区别", () => {
233
+ test("Esc 和 Ctrl+C 都会中断流式输出,但行为不同", () => {
234
+ // 这个测试文档化两种中断方式的区别
235
+
236
+ // Ctrl+C 第一次:中断执行 + 显示 "按 Ctrl+C 再次中断或退出程序"
237
+ // Ctrl+C 第二次(在空闲时):退出程序
238
+
239
+ // Esc:仅在执行时中断 + 显示 "[已中断,可以继续输入]"
240
+ // Esc 在空闲时:不做任何操作
241
+
242
+ // 两种方式都调用:
243
+ // 1. AgentComponent.abort("default") - 中断底层 LLM 调用
244
+ // 2. abortStream() - 停止流式输出显示
245
+ // 3. isExecuting = false - 重置状态
246
+ // 4. rl.prompt(true) - 恢复 prompt
247
+
248
+ // 不同点:
249
+ // - Esc 在空闲时不做任何操作
250
+ // - Ctrl+C 在空闲时会退出程序
251
+
252
+ expect(true).toBe(true); // 文档化测试
253
+ });
254
+ });