@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,233 @@
1
+ /**
2
+ * @fileoverview Debug Log Command - TDD Tests
3
+ *
4
+ * TDD 开发流程:
5
+ * 1. RED - 编写失败的测试
6
+ * 2. GREEN - 编写最小代码让测试通过
7
+ * 3. REFACTOR - 重构
8
+ *
9
+ * 测试目标: debug log --tail -n xxx
10
+ * 功能: 读取 log-trace component 的日志文件配置的尾部 n 行
11
+ */
12
+
13
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+
17
+ describe("Debug Log Command - TDD", () => {
18
+ let tempDir: string;
19
+ let tempLogFile: string;
20
+
21
+ beforeEach(() => {
22
+ // Create temp directory and file
23
+ tempDir = fs.mkdtempSync("/tmp/debug-log-test-");
24
+ tempLogFile = path.join(tempDir, "test.log");
25
+ });
26
+
27
+ afterEach(() => {
28
+ // Cleanup
29
+ try {
30
+ if (fs.existsSync(tempLogFile)) {
31
+ fs.unlinkSync(tempLogFile);
32
+ }
33
+ if (fs.existsSync(tempDir)) {
34
+ fs.rmSync(tempDir, { recursive: true });
35
+ }
36
+ } catch {
37
+ // Ignore cleanup errors
38
+ }
39
+ });
40
+
41
+ describe("RED: getDefaultLogFile()", () => {
42
+ test("should return path in XDG_DATA_HOME/roy-agent/logs/app.log by default", async () => {
43
+ const { getDefaultLogFile } = await import("./log");
44
+
45
+ // Clear environment variables
46
+ delete process.env.LOG_TRACE_LOGGING_DIR;
47
+ delete process.env.LOG_TRACE_LOGGING_FILE;
48
+
49
+ const logFile = getDefaultLogFile();
50
+
51
+ expect(logFile).toContain("roy-agent");
52
+ expect(logFile).toContain("logs");
53
+ expect(logFile).toContain("app.log");
54
+ });
55
+
56
+ test("should use LOG_TRACE_LOGGING_DIR environment variable when set", async () => {
57
+ const { getDefaultLogFile } = await import("./log");
58
+
59
+ const customDir = "/custom/logs/dir";
60
+ process.env.LOG_TRACE_LOGGING_DIR = customDir;
61
+
62
+ const logFile = getDefaultLogFile();
63
+
64
+ expect(logFile).toStartWith(customDir);
65
+ expect(logFile).toEndWith("app.log");
66
+
67
+ delete process.env.LOG_TRACE_LOGGING_DIR;
68
+ });
69
+
70
+ test("should use LOG_TRACE_LOGGING_FILE environment variable when set to absolute path", async () => {
71
+ const { getDefaultLogFile } = await import("./log");
72
+
73
+ const customFile = "/absolute/path/custom.log";
74
+ process.env.LOG_TRACE_LOGGING_FILE = customFile;
75
+
76
+ const logFile = getDefaultLogFile();
77
+
78
+ expect(logFile).toBe(customFile);
79
+
80
+ delete process.env.LOG_TRACE_LOGGING_FILE;
81
+ });
82
+
83
+ test("should use custom filename in default log dir when LOG_TRACE_LOGGING_FILE is just filename", async () => {
84
+ const { getDefaultLogFile } = await import("./log");
85
+
86
+ process.env.LOG_TRACE_LOGGING_DIR = "/custom/logs";
87
+ process.env.LOG_TRACE_LOGGING_FILE = "custom-app.log";
88
+
89
+ const logFile = getDefaultLogFile();
90
+
91
+ expect(logFile).toBe("/custom/logs/custom-app.log");
92
+
93
+ delete process.env.LOG_TRACE_LOGGING_DIR;
94
+ delete process.env.LOG_TRACE_LOGGING_FILE;
95
+ });
96
+ });
97
+
98
+ describe("RED: readTailLines()", () => {
99
+ test("should read last n lines from file", async () => {
100
+ // Write test file
101
+ const lines = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`);
102
+ fs.writeFileSync(tempLogFile, lines.join("\n"));
103
+
104
+ const { readTailLines } = await import("./log");
105
+
106
+ const tailLines = await readTailLines(tempLogFile, 10);
107
+
108
+ expect(tailLines).toHaveLength(10);
109
+ expect(tailLines[0]).toBe("Line 91");
110
+ expect(tailLines[9]).toBe("Line 100");
111
+ });
112
+
113
+ test("should return all lines if file has fewer lines than n", async () => {
114
+ const lines = ["Line 1", "Line 2", "Line 3"];
115
+ fs.writeFileSync(tempLogFile, lines.join("\n"));
116
+
117
+ const { readTailLines } = await import("./log");
118
+
119
+ const tailLines = await readTailLines(tempLogFile, 10);
120
+
121
+ expect(tailLines).toHaveLength(3);
122
+ expect(tailLines[0]).toBe("Line 1");
123
+ });
124
+
125
+ test("should return empty array for empty file", async () => {
126
+ fs.writeFileSync(tempLogFile, "");
127
+
128
+ const { readTailLines } = await import("./log");
129
+
130
+ const tailLines = await readTailLines(tempLogFile, 10);
131
+
132
+ expect(tailLines).toHaveLength(0);
133
+ });
134
+
135
+ test("should handle file with trailing newline", async () => {
136
+ const lines = ["Line 1", "Line 2", "Line 3"];
137
+ fs.writeFileSync(tempLogFile, lines.join("\n") + "\n");
138
+
139
+ const { readTailLines } = await import("./log");
140
+
141
+ const tailLines = await readTailLines(tempLogFile, 3);
142
+
143
+ // readline treats each newline as end of line
144
+ expect(tailLines).toHaveLength(3);
145
+ });
146
+
147
+ test("should handle n=0 and return empty array", async () => {
148
+ const lines = ["Line 1", "Line 2", "Line 3"];
149
+ fs.writeFileSync(tempLogFile, lines.join("\n"));
150
+
151
+ const { readTailLines } = await import("./log");
152
+
153
+ const tailLines = await readTailLines(tempLogFile, 0);
154
+
155
+ expect(tailLines).toHaveLength(0);
156
+ });
157
+
158
+ test("should reject promise when file does not exist", async () => {
159
+ const { readTailLines } = await import("./log");
160
+
161
+ await expect(readTailLines("/non/existent/file.log", 10)).rejects.toThrow();
162
+ });
163
+ });
164
+
165
+ describe("GREEN: LogCommand structure", () => {
166
+ test("should have correct command name 'log'", async () => {
167
+ const { LogCommand } = await import("./index");
168
+
169
+ expect(LogCommand.command).toBe("log");
170
+ });
171
+
172
+ test("should have describe text mentioning tail functionality", async () => {
173
+ const { LogCommand } = await import("./index");
174
+
175
+ expect(LogCommand.describe).toContain("tail");
176
+ });
177
+
178
+ test("should have builder that adds --tail option", async () => {
179
+ const { LogCommand } = await import("./index");
180
+
181
+ let capturedOptions: Record<string, any> = {};
182
+ let optionCalls = 0;
183
+ const mockYargs = {
184
+ option: (name: string, config: any) => {
185
+ optionCalls++;
186
+ capturedOptions[name] = config;
187
+ return mockYargs;
188
+ },
189
+ };
190
+
191
+ LogCommand.builder(mockYargs as any);
192
+
193
+ expect(optionCalls).toBeGreaterThanOrEqual(1);
194
+ expect(capturedOptions.tail).toBeDefined();
195
+ expect(capturedOptions.tail.alias).toBe("n");
196
+ expect(capturedOptions.tail.type).toBe("number");
197
+ });
198
+
199
+ test("should have --tail option with default value 50", async () => {
200
+ const { LogCommand } = await import("./index");
201
+
202
+ let capturedOptions: Record<string, any> = {};
203
+ const mockYargs = {
204
+ option: (name: string, config: any) => {
205
+ capturedOptions[name] = config;
206
+ return mockYargs;
207
+ },
208
+ };
209
+
210
+ LogCommand.builder(mockYargs as any);
211
+
212
+ expect(capturedOptions.tail.default).toBe(50);
213
+ });
214
+
215
+ test("should have --file option for custom log file path", async () => {
216
+ const { LogCommand } = await import("./index");
217
+
218
+ let capturedOptions: Record<string, any> = {};
219
+ const mockYargs = {
220
+ option: (name: string, config: any) => {
221
+ capturedOptions[name] = config;
222
+ return mockYargs;
223
+ },
224
+ };
225
+
226
+ LogCommand.builder(mockYargs as any);
227
+
228
+ expect(capturedOptions.file).toBeDefined();
229
+ expect(capturedOptions.file.alias).toBe("f");
230
+ expect(capturedOptions.file.type).toBe("string");
231
+ });
232
+ });
233
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview Debug Log Command
3
+ *
4
+ * Command: roy debug log --tail -n <lines>
5
+ *
6
+ * 读取 log-trace component 日志配置对应的日志文件,显示尾部 n 行
7
+ */
8
+
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as readline from "readline";
12
+ import type { CommandModule } from "yargs";
13
+
14
+ interface LogOptions {
15
+ tail?: number;
16
+ file?: string;
17
+ }
18
+
19
+ export const LogCommand: CommandModule<object, LogOptions> = {
20
+ command: "log",
21
+ describe: "查看日志文件(支持 --tail -n 显示尾部行)",
22
+
23
+ builder: (yargs) =>
24
+ yargs
25
+ .option("tail", {
26
+ alias: "n",
27
+ type: "number",
28
+ description: "显示尾部 n 行(默认 50)",
29
+ default: 50,
30
+ })
31
+ .option("file", {
32
+ alias: "f",
33
+ type: "string",
34
+ description: "日志文件路径(可选,默认使用配置路径)",
35
+ }),
36
+
37
+ async handler(args) {
38
+ // 获取日志文件路径
39
+ const logFile = args.file || getDefaultLogFile();
40
+
41
+ if (!fs.existsSync(logFile)) {
42
+ console.error(`Log file not found: ${logFile}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ try {
47
+ const lines = await readTailLines(logFile, args.tail || 50);
48
+
49
+ if (lines.length === 0) {
50
+ console.log("(empty log file)");
51
+ return;
52
+ }
53
+
54
+ // 逐行输出
55
+ for (const line of lines) {
56
+ console.log(line);
57
+ }
58
+ } catch (error) {
59
+ console.error(`Failed to read log file: ${error}`);
60
+ process.exit(1);
61
+ }
62
+ },
63
+ };
64
+
65
+ /**
66
+ * 读取文件尾部 n 行
67
+ */
68
+ export async function readTailLines(filePath: string, n: number): Promise<string[]> {
69
+ return new Promise((resolve, reject) => {
70
+ const lines: string[] = [];
71
+ const rl = readline.createInterface({
72
+ input: fs.createReadStream(filePath),
73
+ crlfDelay: Infinity,
74
+ });
75
+
76
+ rl.on("line", (line) => {
77
+ lines.push(line);
78
+ // 只保留尾部 n 行
79
+ if (lines.length > n) {
80
+ lines.shift();
81
+ }
82
+ });
83
+
84
+ rl.on("close", () => {
85
+ resolve(lines);
86
+ });
87
+
88
+ rl.on("error", (err) => {
89
+ reject(err);
90
+ });
91
+ });
92
+ }
93
+
94
+ /**
95
+ * 获取默认日志文件路径
96
+ *
97
+ * 使用 log-trace component 的配置:
98
+ * - log-trace.logging.dir: 日志目录
99
+ * - log-trace.logging.file: 日志文件名
100
+ *
101
+ * 默认: ~/.local/share/roy-agent/logs/app.log
102
+ */
103
+ export function getDefaultLogFile(): string {
104
+ // 支持环境变量覆盖
105
+ if (process.env.LOG_TRACE_LOGGING_DIR) {
106
+ const dir = process.env.LOG_TRACE_LOGGING_DIR;
107
+ const file = process.env.LOG_TRACE_LOGGING_FILE || "app.log";
108
+ return path.join(dir, file);
109
+ }
110
+
111
+ // 支持直接指定文件路径
112
+ if (process.env.LOG_TRACE_LOGGING_FILE?.includes("/") || process.env.LOG_TRACE_LOGGING_FILE?.includes("\\")) {
113
+ return process.env.LOG_TRACE_LOGGING_FILE;
114
+ }
115
+
116
+ // 使用 XDG 标准路径
117
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
118
+ const dataHome = process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
119
+ const logDir = path.join(dataHome, "roy-agent", "logs");
120
+ const logFile = process.env.LOG_TRACE_LOGGING_FILE || "app.log";
121
+
122
+ return path.join(logDir, logFile);
123
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @fileoverview Debug Span Command - TDD Tests
3
+ *
4
+ * TDD 开发流程:
5
+ * 1. RED - 编写失败的测试
6
+ * 2. GREEN - 编写最小代码让测试通过
7
+ * 3. REFACTOR - 重构
8
+ *
9
+ * 测试目标: roy debug span <spanId>
10
+ * 功能: 通过 spanId 从 SQLite 中查询详细的 span 信息
11
+ */
12
+
13
+ import { describe, expect, test } from "bun:test";
14
+ import type { Span } from "@ai-setting/roy-agent-core";
15
+ import { SpanKind, SpanStatus } from "@ai-setting/roy-agent-core";
16
+
17
+ describe("Debug Span Command - TDD", () => {
18
+ describe("RED: formatSpanDetail()", () => {
19
+ test("should format span with all fields", async () => {
20
+ const { formatSpanDetail } = await import("./span");
21
+
22
+ const span: Span = {
23
+ traceId: "trace_1",
24
+ spanId: "span_123",
25
+ parentSpanId: "span_parent",
26
+ name: "test-operation",
27
+ kind: SpanKind.INTERNAL,
28
+ status: SpanStatus.OK,
29
+ startTime: 1000000000000,
30
+ endTime: 1000000001500,
31
+ attributes: { "http.method": "GET", "http.url": "/api/test" },
32
+ result: { data: "success" },
33
+ children: [],
34
+ };
35
+
36
+ const output = formatSpanDetail(span);
37
+
38
+ expect(output).toContain("span_123");
39
+ expect(output).toContain("test-operation");
40
+ expect(output).toContain("trace_1");
41
+ expect(output).toContain("500ms"); // duration
42
+ });
43
+
44
+ test("should show running span without duration", async () => {
45
+ const { formatSpanDetail } = await import("./span");
46
+
47
+ const span: Span = {
48
+ traceId: "trace_1",
49
+ spanId: "span_running",
50
+ name: "running-operation",
51
+ kind: SpanKind.INTERNAL,
52
+ status: SpanStatus.OK,
53
+ startTime: 1000000000000,
54
+ endTime: undefined,
55
+ attributes: {},
56
+ children: [],
57
+ };
58
+
59
+ const output = formatSpanDetail(span);
60
+
61
+ expect(output).toContain("span_running");
62
+ expect(output).toContain("(running)");
63
+ });
64
+
65
+ test("should show error span with error message", async () => {
66
+ const { formatSpanDetail } = await import("./span");
67
+
68
+ const span: Span = {
69
+ traceId: "trace_1",
70
+ spanId: "span_error",
71
+ name: "failed-operation",
72
+ kind: SpanKind.INTERNAL,
73
+ status: SpanStatus.ERROR,
74
+ startTime: 1000000000000,
75
+ endTime: 1000000001000,
76
+ attributes: {},
77
+ error: "Something went wrong",
78
+ children: [],
79
+ };
80
+
81
+ const output = formatSpanDetail(span);
82
+
83
+ // status 是小写的
84
+ expect(output).toContain("Status: error");
85
+ expect(output).toContain("Something went wrong");
86
+ });
87
+
88
+ test("should format attributes as key-value pairs", async () => {
89
+ const { formatSpanDetail } = await import("./span");
90
+
91
+ const span: Span = {
92
+ traceId: "trace_1",
93
+ spanId: "span_attrs",
94
+ name: "operation-with-attrs",
95
+ kind: SpanKind.INTERNAL,
96
+ status: SpanStatus.OK,
97
+ startTime: 1000000000000,
98
+ endTime: 1000000001000,
99
+ attributes: {
100
+ "http.method": "POST",
101
+ "http.url": "/api/users",
102
+ "http.status_code": 200
103
+ },
104
+ children: [],
105
+ };
106
+
107
+ const output = formatSpanDetail(span);
108
+
109
+ expect(output).toContain("http.method");
110
+ expect(output).toContain("POST");
111
+ expect(output).toContain("http.url");
112
+ expect(output).toContain("/api/users");
113
+ });
114
+
115
+ test("should show result if present", async () => {
116
+ const { formatSpanDetail } = await import("./span");
117
+
118
+ const span: Span = {
119
+ traceId: "trace_1",
120
+ spanId: "span_result",
121
+ name: "operation-with-result",
122
+ kind: SpanKind.INTERNAL,
123
+ status: SpanStatus.OK,
124
+ startTime: 1000000000000,
125
+ endTime: 1000000001000,
126
+ attributes: {},
127
+ result: { items: ["a", "b", "c"], count: 3 },
128
+ children: [],
129
+ };
130
+
131
+ const output = formatSpanDetail(span);
132
+
133
+ expect(output).toContain("Result:");
134
+ expect(output).toContain('"a"');
135
+ expect(output).toContain("count");
136
+ expect(output).toContain("3");
137
+ });
138
+
139
+ test("should show parent span info", async () => {
140
+ const { formatSpanDetail } = await import("./span");
141
+
142
+ const span: Span = {
143
+ traceId: "trace_1",
144
+ spanId: "span_child",
145
+ parentSpanId: "span_parent",
146
+ name: "child-operation",
147
+ kind: SpanKind.INTERNAL,
148
+ status: SpanStatus.OK,
149
+ startTime: 1000000000000,
150
+ endTime: 1000000001000,
151
+ attributes: {},
152
+ children: [],
153
+ };
154
+
155
+ const output = formatSpanDetail(span);
156
+
157
+ // 首字母大写格式
158
+ expect(output).toContain("Parent:");
159
+ expect(output).toContain("span_parent");
160
+ });
161
+
162
+ test("should show kind information", async () => {
163
+ const { formatSpanDetail } = await import("./span");
164
+
165
+ const span: Span = {
166
+ traceId: "trace_1",
167
+ spanId: "span_kind",
168
+ name: "server-handler",
169
+ kind: SpanKind.SERVER,
170
+ status: SpanStatus.OK,
171
+ startTime: 1000000000000,
172
+ endTime: 1000000001000,
173
+ attributes: {},
174
+ children: [],
175
+ };
176
+
177
+ const output = formatSpanDetail(span);
178
+
179
+ // 首字母大写格式,值是小写的
180
+ expect(output).toContain("Kind:");
181
+ expect(output).toContain("server");
182
+ });
183
+
184
+ test("should handle circular reference in result", async () => {
185
+ const { formatSpanDetail } = await import("./span");
186
+
187
+ // 创建循环引用对象
188
+ const circularObj: any = { name: "test" };
189
+ circularObj.self = circularObj;
190
+
191
+ const span: Span = {
192
+ traceId: "trace_1",
193
+ spanId: "span_circular",
194
+ name: "operation",
195
+ kind: SpanKind.INTERNAL,
196
+ status: SpanStatus.OK,
197
+ startTime: 1000000000000,
198
+ endTime: 1000000001000,
199
+ attributes: {},
200
+ result: circularObj,
201
+ children: [],
202
+ };
203
+
204
+ // Should not throw, should handle circular reference
205
+ expect(() => formatSpanDetail(span)).not.toThrow();
206
+ const output = formatSpanDetail(span);
207
+ expect(output).toContain("Result:");
208
+ expect(output).toContain("[Circular]");
209
+ });
210
+
211
+ test("should handle corrupted result string from old data", async () => {
212
+ const { formatSpanDetail } = await import("./span");
213
+
214
+ // 模拟已经存储的损坏数据
215
+ const span: Span = {
216
+ traceId: "trace_1",
217
+ spanId: "span_corrupted",
218
+ name: "operation",
219
+ kind: SpanKind.INTERNAL,
220
+ status: SpanStatus.OK,
221
+ startTime: 1000000000000,
222
+ endTime: 1000000001000,
223
+ attributes: {},
224
+ result: "[Object with circular reference: some error message]" as any,
225
+ children: [],
226
+ };
227
+
228
+ const output = formatSpanDetail(span);
229
+ expect(output).toContain("Result:");
230
+ expect(output).toContain("Circular reference");
231
+ });
232
+ });
233
+
234
+ describe("GREEN: SpanCommand structure", () => {
235
+ test("should have correct command name 'span'", async () => {
236
+ const { SpanCommand } = await import("./index");
237
+
238
+ expect(SpanCommand.command).toBe("span");
239
+ });
240
+
241
+ test("should have describe text mentioning span lookup", async () => {
242
+ const { SpanCommand } = await import("./index");
243
+
244
+ expect(SpanCommand.describe).toBeDefined();
245
+ expect(typeof SpanCommand.describe).toBe("string");
246
+ expect((SpanCommand.describe || "").toLowerCase()).toContain("span");
247
+ });
248
+
249
+ test("should have builder that adds spanId positional argument", async () => {
250
+ const { SpanCommand } = await import("./index");
251
+
252
+ let positionalConfig: any = null;
253
+ const mockYargs = {
254
+ positional: (name: string, config: any) => {
255
+ if (name === "spanId") {
256
+ positionalConfig = config;
257
+ }
258
+ return mockYargs;
259
+ },
260
+ option: () => mockYargs,
261
+ };
262
+
263
+ SpanCommand.builder(mockYargs as any);
264
+
265
+ expect(positionalConfig).toBeDefined();
266
+ expect(positionalConfig.type).toBe("string");
267
+ });
268
+
269
+ test("should have --db option for custom db path", async () => {
270
+ const { SpanCommand } = await import("./index");
271
+
272
+ let capturedOptions: Record<string, any> = {};
273
+ const mockYargs = {
274
+ option: (name: string, config: any) => {
275
+ capturedOptions[name] = config;
276
+ return mockYargs;
277
+ },
278
+ positional: () => mockYargs,
279
+ };
280
+
281
+ SpanCommand.builder(mockYargs as any);
282
+
283
+ expect(capturedOptions.db).toBeDefined();
284
+ expect(capturedOptions.db.type).toBe("string");
285
+ });
286
+ });
287
+
288
+ describe("GREEN: SpanCommand handler integration", () => {
289
+ test("should be exported from debug/index.ts", async () => {
290
+ // This test verifies the span command is properly integrated
291
+ const debugModule = await import("./index");
292
+
293
+ expect(debugModule.SpanCommand).toBeDefined();
294
+ expect(debugModule.SpanCommand.command).toBe("span");
295
+ });
296
+ });
297
+ });