chatccc 0.2.252 → 0.2.254

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 (296) hide show
  1. package/README.md +10 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/config.sample.json +2 -1
  5. package/deepccc-agent/README.md +40 -35
  6. package/deepccc-agent/os-prompts/darwin.md +8 -8
  7. package/deepccc-agent/os-prompts/linux.md +8 -8
  8. package/deepccc-agent/os-prompts/win32.md +11 -11
  9. package/deepccc-agent/package.json +65 -65
  10. package/dist/deepccc-agent/src/cli.js +634 -0
  11. package/dist/deepccc-agent/src/config.js +81 -0
  12. package/dist/deepccc-agent/src/context.js +348 -0
  13. package/dist/deepccc-agent/src/file-log.js +34 -0
  14. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  15. package/dist/deepccc-agent/src/index.js +601 -0
  16. package/dist/deepccc-agent/src/permissions.js +170 -0
  17. package/dist/deepccc-agent/src/privacy.js +124 -0
  18. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  19. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  20. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  21. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  22. package/dist/deepccc-agent/src/progress/view.js +30 -0
  23. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  24. package/dist/deepccc-agent/src/session-search.js +276 -0
  25. package/dist/deepccc-agent/src/session-select.js +23 -0
  26. package/dist/deepccc-agent/src/sigint.js +26 -0
  27. package/dist/deepccc-agent/src/skills.js +178 -0
  28. package/dist/deepccc-agent/src/web-tools.js +246 -0
  29. package/dist/src/adapters/adapter-interface.js +19 -0
  30. package/dist/src/adapters/ccc-adapter.js +112 -0
  31. package/dist/src/adapters/claude-adapter.js +497 -0
  32. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  33. package/dist/src/adapters/codex-adapter.js +279 -0
  34. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  35. package/dist/src/adapters/cursor-adapter.js +491 -0
  36. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  37. package/dist/src/adapters/jsonl-stream.js +104 -0
  38. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  39. package/dist/src/adapters/raw-stream-log.js +106 -0
  40. package/dist/src/adapters/resource-monitor.js +113 -0
  41. package/dist/src/agent-activity.js +133 -0
  42. package/dist/src/agent-delegate-task-rpc.js +129 -0
  43. package/dist/src/agent-delegate-task.js +48 -0
  44. package/dist/src/agent-file-rpc.js +152 -0
  45. package/dist/src/agent-image-rpc.js +148 -0
  46. package/dist/src/agent-platform-routing.js +13 -0
  47. package/dist/src/agent-reload-config-rpc.js +23 -0
  48. package/dist/src/agent-rpc-body.js +87 -0
  49. package/dist/src/agent-stop-stuck.js +110 -0
  50. package/dist/src/agent-team/application/board-service.js +142 -0
  51. package/dist/src/agent-team/application/main-agent-service.js +151 -0
  52. package/dist/src/agent-team/domain/board.js +69 -0
  53. package/dist/src/agent-team/http/board-routes.js +213 -0
  54. package/dist/src/agent-team/infrastructure/filesystem-browser.js +112 -0
  55. package/dist/src/agent-team/infrastructure/main-agent-session-runtime.js +30 -0
  56. package/dist/src/agent-team/main-agent-bootstrap.js +15 -0
  57. package/dist/src/agent-team/repositories/board-repository.js +10 -0
  58. package/dist/src/agent-team/repositories/feishu-p2p-contact-store.js +64 -0
  59. package/dist/src/agent-team/repositories/json-board-repository.js +209 -0
  60. package/dist/src/agent-team/repositories/main-agent-binding-repository.js +84 -0
  61. package/dist/src/agent-team/sync/board-sync-adapter.js +1 -0
  62. package/dist/src/agent-team/web/agent-team-page.js +219 -0
  63. package/dist/src/agent-tool.js +10 -0
  64. package/dist/src/card-action-routing.js +7 -0
  65. package/dist/src/card-plain-text.js +101 -0
  66. package/dist/src/cardkit.js +158 -0
  67. package/dist/src/cards.js +573 -0
  68. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  69. package/dist/src/chatgpt-subscription.js +199 -0
  70. package/dist/src/chrome-devtools-guard.js +238 -0
  71. package/dist/src/claude-sdk-installer.js +249 -0
  72. package/dist/src/codex-reset-actions.js +143 -0
  73. package/dist/src/config-utils.js +149 -0
  74. package/dist/src/config.js +808 -0
  75. package/dist/src/cursor-usage.js +77 -0
  76. package/dist/src/exit-banner.js +28 -0
  77. package/dist/src/feishu-api.js +1404 -0
  78. package/dist/src/feishu-message-ingress.js +137 -0
  79. package/dist/src/feishu-platform.js +97 -0
  80. package/dist/src/format-message.js +252 -0
  81. package/dist/src/git-command.js +155 -0
  82. package/dist/src/im-skills.js +121 -0
  83. package/dist/src/index.js +835 -0
  84. package/dist/src/litellm-proxy.js +300 -0
  85. package/dist/src/orchestrator.js +2096 -0
  86. package/dist/src/package-root.js +26 -0
  87. package/dist/src/platform-adapter.js +7 -0
  88. package/dist/src/platform-startup.js +6 -0
  89. package/dist/src/privacy.js +100 -0
  90. package/dist/src/progress/reducer.js +102 -0
  91. package/dist/src/progress/terminal-renderer.js +264 -0
  92. package/dist/src/progress/view.js +30 -0
  93. package/dist/src/response-stall.js +14 -0
  94. package/dist/src/runtime-entry.js +13 -0
  95. package/dist/src/runtime-reload.js +19 -0
  96. package/dist/src/session-chat-binding.js +183 -0
  97. package/dist/src/session-name.js +7 -0
  98. package/dist/src/session.js +2147 -0
  99. package/dist/src/shared-prefix.js +16 -0
  100. package/dist/src/shared.js +493 -0
  101. package/dist/src/sim-agent.js +105 -0
  102. package/dist/src/sim-platform.js +142 -0
  103. package/dist/src/sim-store.js +231 -0
  104. package/dist/src/simplify.js +99 -0
  105. package/dist/src/startup-lifecycle.js +209 -0
  106. package/dist/src/stream-state.js +141 -0
  107. package/dist/src/terminal-error.js +100 -0
  108. package/dist/src/trace.js +50 -0
  109. package/dist/src/turn-cards.js +92 -0
  110. package/dist/src/update-command-guard.js +114 -0
  111. package/{src/web-ui.ts → dist/src/web-ui.js} +753 -839
  112. package/dist/src/wechat-platform.js +545 -0
  113. package/package.json +76 -74
  114. package/deepccc-agent/LICENSE +0 -201
  115. package/deepccc-agent/bin/deepccc.mjs +0 -26
  116. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  117. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  118. package/deepccc-agent/package-lock.json +0 -2027
  119. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  120. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  121. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  122. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  123. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  124. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  125. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  126. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  127. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  128. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  129. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  130. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  131. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  132. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  133. package/deepccc-agent/src/cli.ts +0 -682
  134. package/deepccc-agent/src/config.ts +0 -101
  135. package/deepccc-agent/src/context.ts +0 -465
  136. package/deepccc-agent/src/file-log.ts +0 -38
  137. package/deepccc-agent/src/file-tools.ts +0 -1493
  138. package/deepccc-agent/src/index.ts +0 -710
  139. package/deepccc-agent/src/permissions.ts +0 -226
  140. package/deepccc-agent/src/privacy.ts +0 -141
  141. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  142. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  143. package/deepccc-agent/src/progress/reducer.ts +0 -113
  144. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  145. package/deepccc-agent/src/progress/view.ts +0 -77
  146. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  147. package/deepccc-agent/src/session-search.ts +0 -370
  148. package/deepccc-agent/src/session-select.ts +0 -48
  149. package/deepccc-agent/src/sigint.ts +0 -50
  150. package/deepccc-agent/src/skills.ts +0 -205
  151. package/deepccc-agent/src/web-tools.ts +0 -313
  152. package/deepccc-agent/tsconfig.build.json +0 -13
  153. package/deepccc-agent/tsconfig.json +0 -13
  154. package/deepccc-agent/vitest.config.ts +0 -7
  155. package/src/__tests__/adapter-interface.test.ts +0 -152
  156. package/src/__tests__/agent-activity.test.ts +0 -86
  157. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  158. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  159. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  160. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  161. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  162. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  163. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  164. package/src/__tests__/builtin-config.test.ts +0 -26
  165. package/src/__tests__/builtin-context.test.ts +0 -319
  166. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  167. package/src/__tests__/builtin-permissions.test.ts +0 -219
  168. package/src/__tests__/builtin-session-search.test.ts +0 -262
  169. package/src/__tests__/builtin-session-select.test.ts +0 -116
  170. package/src/__tests__/builtin-sigint.test.ts +0 -56
  171. package/src/__tests__/builtin-skills.test.ts +0 -284
  172. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  173. package/src/__tests__/card-action-routing.test.ts +0 -18
  174. package/src/__tests__/card-plain-text.test.ts +0 -45
  175. package/src/__tests__/cardkit.test.ts +0 -60
  176. package/src/__tests__/cards.test.ts +0 -607
  177. package/src/__tests__/ccc-adapter.test.ts +0 -194
  178. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  179. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  180. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  181. package/src/__tests__/claude-adapter.test.ts +0 -614
  182. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  183. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  184. package/src/__tests__/codex-adapter.test.ts +0 -331
  185. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  186. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  187. package/src/__tests__/config-reload.test.ts +0 -284
  188. package/src/__tests__/config-sample.test.ts +0 -97
  189. package/src/__tests__/config-utils.test.ts +0 -40
  190. package/src/__tests__/config.test.ts +0 -395
  191. package/src/__tests__/crash-logging.test.ts +0 -360
  192. package/src/__tests__/cursor-adapter.test.ts +0 -890
  193. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  194. package/src/__tests__/feishu-api.test.ts +0 -60
  195. package/src/__tests__/feishu-avatar.test.ts +0 -504
  196. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  197. package/src/__tests__/feishu-platform.test.ts +0 -75
  198. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  199. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  200. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  201. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  202. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  203. package/src/__tests__/format-message.test.ts +0 -316
  204. package/src/__tests__/git-command.test.ts +0 -288
  205. package/src/__tests__/im-skills.test.ts +0 -125
  206. package/src/__tests__/jsonl-stream.test.ts +0 -79
  207. package/src/__tests__/orchestrator.test.ts +0 -1268
  208. package/src/__tests__/package-files.test.ts +0 -24
  209. package/src/__tests__/platform-startup.test.ts +0 -19
  210. package/src/__tests__/privacy.test.ts +0 -198
  211. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  212. package/src/__tests__/progress-reducer.test.ts +0 -121
  213. package/src/__tests__/raw-stream-log.test.ts +0 -106
  214. package/src/__tests__/response-stall.test.ts +0 -49
  215. package/src/__tests__/restart.test.ts +0 -232
  216. package/src/__tests__/session-ccc-config.test.ts +0 -66
  217. package/src/__tests__/session.test.ts +0 -3004
  218. package/src/__tests__/shared-prefix.test.ts +0 -36
  219. package/src/__tests__/sim-agent.test.ts +0 -174
  220. package/src/__tests__/sim-platform.test.ts +0 -93
  221. package/src/__tests__/sim-store.test.ts +0 -214
  222. package/src/__tests__/simplify.test.ts +0 -283
  223. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  224. package/src/__tests__/stop-session.test.ts +0 -162
  225. package/src/__tests__/stream-state.test.ts +0 -164
  226. package/src/__tests__/terminal-error.test.ts +0 -54
  227. package/src/__tests__/terminal-renderer.test.ts +0 -247
  228. package/src/__tests__/update-command-guard.test.ts +0 -144
  229. package/src/__tests__/web-ui.test.ts +0 -438
  230. package/src/__tests__/wechat-platform.test.ts +0 -111
  231. package/src/adapters/adapter-interface.ts +0 -217
  232. package/src/adapters/ccc-adapter.ts +0 -150
  233. package/src/adapters/claude-adapter.ts +0 -673
  234. package/src/adapters/claude-session-meta-store.ts +0 -120
  235. package/src/adapters/codex-adapter.ts +0 -426
  236. package/src/adapters/codex-session-meta-store.ts +0 -131
  237. package/src/adapters/cursor-adapter.ts +0 -681
  238. package/src/adapters/cursor-session-meta-store.ts +0 -154
  239. package/src/adapters/jsonl-stream.ts +0 -157
  240. package/src/adapters/raw-stream-log.ts +0 -124
  241. package/src/adapters/resource-monitor.ts +0 -141
  242. package/src/agent-activity.ts +0 -175
  243. package/src/agent-delegate-task-rpc.ts +0 -153
  244. package/src/agent-delegate-task.ts +0 -91
  245. package/src/agent-file-rpc.ts +0 -172
  246. package/src/agent-image-rpc.ts +0 -168
  247. package/src/agent-platform-routing.ts +0 -28
  248. package/src/agent-reload-config-rpc.ts +0 -34
  249. package/src/agent-rpc-body.ts +0 -92
  250. package/src/agent-stop-stuck.ts +0 -129
  251. package/src/card-action-routing.ts +0 -14
  252. package/src/card-plain-text.ts +0 -108
  253. package/src/cardkit.ts +0 -179
  254. package/src/cards.ts +0 -684
  255. package/src/chatgpt-subscription-rpc.ts +0 -27
  256. package/src/chatgpt-subscription.ts +0 -299
  257. package/src/chrome-devtools-guard.ts +0 -318
  258. package/src/claude-sdk-installer.ts +0 -324
  259. package/src/codex-reset-actions.ts +0 -184
  260. package/src/config-utils.ts +0 -211
  261. package/src/config.ts +0 -1063
  262. package/src/cursor-usage.ts +0 -128
  263. package/src/exit-banner.ts +0 -33
  264. package/src/feishu-api.ts +0 -1616
  265. package/src/feishu-message-ingress.ts +0 -195
  266. package/src/feishu-platform.ts +0 -159
  267. package/src/format-message.ts +0 -293
  268. package/src/git-command.ts +0 -202
  269. package/src/im-skills.ts +0 -149
  270. package/src/index.ts +0 -1089
  271. package/src/litellm-proxy.ts +0 -374
  272. package/src/orchestrator.ts +0 -2543
  273. package/src/platform-adapter.ts +0 -70
  274. package/src/platform-startup.ts +0 -16
  275. package/src/privacy.ts +0 -118
  276. package/src/progress/reducer.ts +0 -113
  277. package/src/progress/terminal-renderer.ts +0 -294
  278. package/src/progress/view.ts +0 -77
  279. package/src/response-stall.ts +0 -28
  280. package/src/runtime-reload.ts +0 -34
  281. package/src/session-chat-binding.ts +0 -292
  282. package/src/session-name.ts +0 -8
  283. package/src/session.ts +0 -2659
  284. package/src/shared-prefix.ts +0 -29
  285. package/src/shared.ts +0 -552
  286. package/src/sim-agent.ts +0 -167
  287. package/src/sim-platform.ts +0 -177
  288. package/src/sim-store.ts +0 -317
  289. package/src/simplify.ts +0 -120
  290. package/src/startup-lifecycle.ts +0 -250
  291. package/src/stream-state.ts +0 -177
  292. package/src/terminal-error.ts +0 -129
  293. package/src/trace.ts +0 -51
  294. package/src/turn-cards.ts +0 -118
  295. package/src/update-command-guard.ts +0 -165
  296. package/src/wechat-platform.ts +0 -680
@@ -1,890 +0,0 @@
1
- import { afterEach, describe, it, expect, vi } from "vitest";
2
- import { EventEmitter } from "node:events";
3
- import { readFileSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { PassThrough } from "node:stream";
6
- import type { ChildProcess } from "node:child_process";
7
- import { fileURLToPath } from "node:url";
8
- import {
9
- normalizeCursorMessage,
10
- createCursorAdapter,
11
- formatCursorAgentEmptyOutputMessage,
12
- } from "../adapters/cursor-adapter.ts";
13
- import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
14
- import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
15
- import type {
16
- CursorSessionMeta,
17
- CursorSessionMetaStore,
18
- } from "../adapters/cursor-session-meta-store.ts";
19
- import { accumulateBlockContent, pickFinalReply, type AccumulatorState } from "../session.ts";
20
-
21
- /**
22
- * 进程内内存版 meta store,用于测试 getSessionInfo / 自动学习行为,避开磁盘 IO。
23
- * 与文件实现行为一致:set 部分合并、空字段不覆盖。
24
- */
25
- function createInMemoryMetaStore(
26
- initial: Record<string, CursorSessionMeta> = {},
27
- ): CursorSessionMetaStore & { snapshot(): Record<string, CursorSessionMeta> } {
28
- const map = new Map<string, CursorSessionMeta>(Object.entries(initial));
29
- return {
30
- async get(sid) {
31
- return map.get(sid);
32
- },
33
- async set(sid, partial) {
34
- const existing = map.get(sid);
35
- const merged: { cwd?: string; model?: string } = { ...existing };
36
- if (typeof partial.cwd === "string" && partial.cwd.length > 0) merged.cwd = partial.cwd;
37
- if (typeof partial.model === "string" && partial.model.length > 0) merged.model = partial.model;
38
- // cwd 缺失时记录视为不完整(与文件 store 一致:get 会返回 undefined)
39
- if (!merged.cwd) return;
40
- map.set(sid, merged.model ? { cwd: merged.cwd, model: merged.model } : { cwd: merged.cwd });
41
- },
42
- snapshot() {
43
- return Object.fromEntries(map);
44
- },
45
- };
46
- }
47
-
48
- const __dirname = dirname(fileURLToPath(import.meta.url));
49
-
50
- type CursorSpawnForTest = NonNullable<
51
- NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
52
- >;
53
-
54
- function readFixture(name: string): unknown[] {
55
- const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
56
- return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
57
- }
58
-
59
- function createMockCursorProcess(args: {
60
- stdout?: string;
61
- stderr?: string;
62
- exitCode?: number;
63
- }): ChildProcess {
64
- const child = new EventEmitter() as ChildProcess;
65
- const stdout = new PassThrough();
66
- const stderr = new PassThrough();
67
- const stdin = new PassThrough();
68
- Object.assign(child, {
69
- stdout,
70
- stderr,
71
- stdin,
72
- pid: undefined,
73
- });
74
-
75
- setImmediate(() => {
76
- if (args.stdout) stdout.write(args.stdout);
77
- if (args.stderr) stderr.write(args.stderr);
78
- stdout.end();
79
- stderr.end();
80
- child.emit("close", args.exitCode ?? 0, null);
81
- });
82
-
83
- return child;
84
- }
85
-
86
- function createHangingMockCursorProcess(args: {
87
- stdout?: string;
88
- stderr?: string;
89
- }): ChildProcess {
90
- const child = new EventEmitter() as ChildProcess;
91
- const stdout = new PassThrough();
92
- const stderr = new PassThrough();
93
- const stdin = new PassThrough();
94
- Object.assign(child, {
95
- stdout,
96
- stderr,
97
- stdin,
98
- pid: undefined,
99
- });
100
-
101
- queueMicrotask(() => {
102
- if (args.stdout) stdout.write(args.stdout);
103
- if (args.stderr) stderr.write(args.stderr);
104
- });
105
-
106
- return child;
107
- }
108
-
109
- afterEach(() => {
110
- vi.useRealTimers();
111
- });
112
-
113
- // ---------------------------------------------------------------------------
114
- // normalizeCursorMessage — 核心映射逻辑测试(纯函数)
115
- // ---------------------------------------------------------------------------
116
-
117
- describe("normalizeCursorMessage", () => {
118
- // --- assistant messages ---
119
-
120
- it("normalizes assistant message with text block (partial 增量)", () => {
121
- // 带 timestamp_ms ⇒ partial(增量片段)⇒ 映射为 text(追加语义)
122
- const result = normalizeCursorMessage({
123
- type: "assistant",
124
- message: {
125
- role: "assistant",
126
- content: [{ type: "text", text: "Hello world" }],
127
- },
128
- timestamp_ms: 1000,
129
- });
130
- expect(result).not.toBeNull();
131
- expect(result!.type).toBe("assistant");
132
- expect(result!.blocks).toEqual([{ type: "text", text: "Hello world" }]);
133
- });
134
-
135
- it("normalizes assistant message with thinking block", () => {
136
- const result = normalizeCursorMessage({
137
- type: "assistant",
138
- message: {
139
- role: "assistant",
140
- content: [{ type: "thinking", thinking: "Let me think..." }],
141
- },
142
- });
143
- expect(result).not.toBeNull();
144
- expect(result!.blocks).toEqual([{ type: "thinking", thinking: "Let me think..." }]);
145
- });
146
-
147
- it("normalizes assistant message with tool_use block", () => {
148
- const result = normalizeCursorMessage({
149
- type: "assistant",
150
- message: {
151
- role: "assistant",
152
- content: [
153
- { type: "tool_use", name: "read_file", input: { filePath: "/tmp/test" } },
154
- ],
155
- },
156
- });
157
- expect(result).not.toBeNull();
158
- expect(result!.blocks).toEqual([
159
- { type: "tool_use", name: "read_file", input: { filePath: "/tmp/test" } },
160
- ]);
161
- });
162
-
163
- it("uses 'unknown' for tool_use without name", () => {
164
- const result = normalizeCursorMessage({
165
- type: "assistant",
166
- message: { role: "assistant", content: [{ type: "tool_use", input: {} }] },
167
- });
168
- expect(result!.blocks[0]).toMatchObject({ type: "tool_use", name: "unknown" });
169
- });
170
-
171
- it("normalizes assistant message with search_result block", () => {
172
- const result = normalizeCursorMessage({
173
- type: "assistant",
174
- message: {
175
- role: "assistant",
176
- content: [{ type: "search_result", query: "cursor docs" }],
177
- },
178
- });
179
- expect(result!.blocks).toEqual([{ type: "search_result", query: "cursor docs" }]);
180
- });
181
-
182
- it("normalizes assistant message with redacted_thinking block", () => {
183
- const result = normalizeCursorMessage({
184
- type: "assistant",
185
- message: { role: "assistant", content: [{ type: "redacted_thinking" }] },
186
- });
187
- expect(result!.blocks).toEqual([{ type: "redacted_thinking" }]);
188
- });
189
-
190
- // --- user messages (tool_result) ---
191
-
192
- it("user 消息中的 text 被丢弃(避免用户输入 echo 污染最终回复)", () => {
193
- const result = normalizeCursorMessage({
194
- type: "user",
195
- message: {
196
- role: "user",
197
- content: [{ type: "text", text: "原始用户输入回放" }],
198
- },
199
- });
200
- expect(result).not.toBeNull();
201
- expect(result!.type).toBe("user");
202
- expect(result!.blocks).toEqual([]);
203
- });
204
-
205
- it("normalizes user message with tool_result block (success)", () => {
206
- const result = normalizeCursorMessage({
207
- type: "user",
208
- message: {
209
- role: "user",
210
- content: [
211
- { type: "tool_result", tool_use_id: "call_abc", content: "done", is_error: false },
212
- ],
213
- },
214
- });
215
- expect(result).not.toBeNull();
216
- expect(result!.type).toBe("user");
217
- expect(result!.blocks).toEqual([
218
- { type: "tool_result", tool_use_id: "call_abc", content: "done", is_error: false },
219
- ]);
220
- });
221
-
222
- it("normalizes user message with tool_result block (error)", () => {
223
- const result = normalizeCursorMessage({
224
- type: "user",
225
- message: {
226
- role: "user",
227
- content: [
228
- { type: "tool_result", tool_use_id: "call_err", content: "fail", is_error: true },
229
- ],
230
- },
231
- });
232
- expect(result!.blocks).toEqual([
233
- { type: "tool_result", tool_use_id: "call_err", content: "fail", is_error: true },
234
- ]);
235
- });
236
-
237
- // --- mixed blocks ---
238
-
239
- it("normalizes message with multiple block types", () => {
240
- const result = normalizeCursorMessage({
241
- type: "assistant",
242
- message: {
243
- role: "assistant",
244
- content: [
245
- { type: "thinking", thinking: "Hmm..." },
246
- { type: "tool_use", name: "search", input: { query: "x" } },
247
- { type: "text", text: "Result found." },
248
- ],
249
- },
250
- timestamp_ms: 1000,
251
- });
252
- expect(result).not.toBeNull();
253
- expect(result!.blocks).toHaveLength(3);
254
- });
255
-
256
- // --- edge cases ---
257
-
258
- // 注:result 消息映射为 text_final 的覆盖见下方 "result 消息" describe 块。
259
-
260
- it("returns null for system init messages", () => {
261
- expect(
262
- normalizeCursorMessage({
263
- type: "system",
264
- subtype: "init",
265
- session_id: "abc",
266
- }),
267
- ).toBeNull();
268
- });
269
-
270
- it("returns null for unknown message types", () => {
271
- expect(normalizeCursorMessage({ type: "unknown_type" })).toBeNull();
272
- expect(normalizeCursorMessage({})).toBeNull();
273
- });
274
-
275
- it("skips unknown block types in content", () => {
276
- const result = normalizeCursorMessage({
277
- type: "assistant",
278
- message: {
279
- role: "assistant",
280
- content: [
281
- { type: "custom_block", data: "ignored" },
282
- { type: "text", text: "visible" },
283
- ],
284
- },
285
- timestamp_ms: 1000,
286
- });
287
- expect(result!.blocks).toEqual([{ type: "text", text: "visible" }]);
288
- });
289
-
290
- it("returns message with empty blocks for empty content array", () => {
291
- const result = normalizeCursorMessage({
292
- type: "assistant",
293
- message: { role: "assistant", content: [] },
294
- });
295
- expect(result).not.toBeNull();
296
- expect(result!.blocks).toEqual([]);
297
- });
298
-
299
- it("returns null for message without content", () => {
300
- const result = normalizeCursorMessage({
301
- type: "assistant",
302
- message: { role: "assistant" },
303
- });
304
- expect(result).toBeNull();
305
- });
306
-
307
- it("skips thinking block with empty string", () => {
308
- const result = normalizeCursorMessage({
309
- type: "assistant",
310
- message: { role: "assistant", content: [{ type: "thinking", thinking: "" }] },
311
- });
312
- expect(result!.blocks).toEqual([]);
313
- });
314
-
315
- it("skips text block with empty string", () => {
316
- const result = normalizeCursorMessage({
317
- type: "assistant",
318
- message: { role: "assistant", content: [{ type: "text", text: "" }] },
319
- });
320
- expect(result!.blocks).toEqual([]);
321
- });
322
-
323
- it("returns null for system message without compact_boundary subtype", () => {
324
- expect(
325
- normalizeCursorMessage({ type: "system", subtype: "notification" }),
326
- ).toBeNull();
327
- });
328
-
329
- it("normalizes system compact_boundary message", () => {
330
- const msg: Record<string, unknown> = {
331
- type: "system",
332
- subtype: "compact_boundary",
333
- compact_metadata: { trigger: "auto", pre_tokens: 15000, post_tokens: 8000 },
334
- };
335
- const result = normalizeCursorMessage(
336
- msg as Parameters<typeof normalizeCursorMessage>[0],
337
- );
338
- expect(result).not.toBeNull();
339
- expect(result!.type).toBe("system");
340
- expect(result!.blocks).toEqual([
341
- { type: "compact_boundary", trigger: "auto", pre_tokens: 15000, post_tokens: 8000 },
342
- ]);
343
- });
344
- });
345
-
346
- // ---------------------------------------------------------------------------
347
- // createCursorAdapter — 工厂函数测试
348
- // ---------------------------------------------------------------------------
349
-
350
- describe("createCursorAdapter", () => {
351
- it("returns adapter with correct displayName and sessionDescPrefix", () => {
352
- const adapter = createCursorAdapter();
353
- expect(adapter.displayName).toBe("Cursor");
354
- expect(adapter.sessionDescPrefix).toBe("Cursor Session:");
355
- });
356
-
357
- it("closeSession does not throw", async () => {
358
- const adapter = createCursorAdapter();
359
- await expect(adapter.closeSession("any-id")).resolves.toBeUndefined();
360
- });
361
-
362
- it("aborts a zero-output createSession stream instead of leaving Cursor Agent alive", async () => {
363
- const store = createInMemoryMetaStore();
364
- const spawnImpl = (() =>
365
- createHangingMockCursorProcess({})) as CursorSpawnForTest;
366
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
367
- const controller = new AbortController();
368
-
369
- const pending = adapter.createSession("F:/repo", controller.signal);
370
- controller.abort();
371
-
372
- await expect(pending).rejects.toThrow("Cursor session creation aborted");
373
- });
374
-
375
- // -------------------------------------------------------------------------
376
- // getSessionInfo 行为契约
377
- // - cwd 决定 /git 是否可用
378
- // - model 决定 /state、/sessions 显示的是否是 Cursor 真实模型
379
- // -------------------------------------------------------------------------
380
-
381
- it("getSessionInfo: store 中无该 sessionId 时只返回 sessionId(cwd / model 都 undefined,让上层走错误分支)", async () => {
382
- const store = createInMemoryMetaStore();
383
- const adapter = createCursorAdapter({ metaStore: store });
384
- const info = await adapter.getSessionInfo("unknown-sid");
385
- expect(info).toEqual({ sessionId: "unknown-sid" });
386
- expect(info?.cwd).toBeUndefined();
387
- expect(info?.model).toBeUndefined();
388
- });
389
-
390
- it("getSessionInfo: store 仅有 cwd 时返回 cwd,model 仍为 undefined(防止显示陈旧/错误模型)", async () => {
391
- const store = createInMemoryMetaStore({ "sid-known": { cwd: "F:/proj/Foo" } });
392
- const adapter = createCursorAdapter({ metaStore: store });
393
- const info = await adapter.getSessionInfo("sid-known");
394
- expect(info).toEqual({ sessionId: "sid-known", cwd: "F:/proj/Foo" });
395
- expect(info?.model).toBeUndefined();
396
- });
397
-
398
- it("getSessionInfo: store 同时有 cwd + model 时一并返回(这是 /state 显示真实模型的关键)", async () => {
399
- const store = createInMemoryMetaStore({
400
- "sid-known": { cwd: "F:/proj/Foo", model: "Composer 2 Fast" },
401
- });
402
- const adapter = createCursorAdapter({ metaStore: store });
403
- const info = await adapter.getSessionInfo("sid-known");
404
- expect(info).toEqual({
405
- sessionId: "sid-known",
406
- cwd: "F:/proj/Foo",
407
- model: "Composer 2 Fast",
408
- });
409
- });
410
-
411
- it("getSessionInfo: 不同 sessionId 互不影响", async () => {
412
- const store = createInMemoryMetaStore({
413
- "sid-A": { cwd: "/a", model: "mA" },
414
- "sid-B": { cwd: "/b" },
415
- });
416
- const adapter = createCursorAdapter({ metaStore: store });
417
- expect(await adapter.getSessionInfo("sid-A")).toEqual({
418
- sessionId: "sid-A",
419
- cwd: "/a",
420
- model: "mA",
421
- });
422
- expect(await adapter.getSessionInfo("sid-B")).toEqual({
423
- sessionId: "sid-B",
424
- cwd: "/b",
425
- });
426
- expect(await adapter.getSessionInfo("sid-C")).toEqual({ sessionId: "sid-C" });
427
- });
428
- });
429
-
430
- // ---------------------------------------------------------------------------
431
- // partial vs final 区分(防止最终消息重复出现两段)
432
- // ---------------------------------------------------------------------------
433
- //
434
- // Cursor CLI 在 `--stream-partial-output` 模式下:
435
- // - partial 消息带 `timestamp_ms` 字段,content 是 delta 增量
436
- // - 流结束时再发一条 final 消息(无 `timestamp_ms`),content 是完整文本
437
- // 若 normalize 时不区分这两种,accumulateBlockContent 会把所有 text 累加,
438
- // 导致最终 finalText = (partial 拼接的完整) + (final 完整) ⇒ 同一段重复两次。
439
- // ---------------------------------------------------------------------------
440
-
441
- // ---------------------------------------------------------------------------
442
- // Cursor stream-json 三类 assistant 事件 + result 消息的区分
443
- //
444
- // 官方文档:cursor.com/docs/cli/reference/output-format
445
- // ┌───────────────────┬───────────────┬─────────────────┐
446
- // │ 种类 │ timestamp_ms │ model_call_id │
447
- // ├───────────────────┼───────────────┼─────────────────┤
448
- // │ Streaming delta │ 有 │ 无 │ ← 唯一带新文本
449
- // │ Buffered flush │ 有 │ 有 │ ← 重复(工具调用前快照)
450
- // │ Final flush │ 无 │ 无 │ ← 重复(回合末快照)
451
- // └───────────────────┴───────────────┴─────────────────┘
452
- // 文档建议:取最终结果应直接读 result 消息的 result 字段。
453
- // ---------------------------------------------------------------------------
454
-
455
- describe("normalizeCursorMessage - 三类 assistant 事件区分", () => {
456
- it("Streaming delta(has timestamp_ms, no model_call_id)→ text(追加)", () => {
457
- const result = normalizeCursorMessage({
458
- type: "assistant",
459
- message: { role: "assistant", content: [{ type: "text", text: "你" }] },
460
- timestamp_ms: 1778411927583,
461
- });
462
- expect(result).not.toBeNull();
463
- expect(result!.blocks).toEqual([{ type: "text", text: "你" }]);
464
- expect(result!.isFinalResponse).toBeUndefined();
465
- });
466
-
467
- it("Buffered flush(has timestamp_ms, has model_call_id)→ text_final(覆盖,避免与 delta 重复累加)", () => {
468
- const result = normalizeCursorMessage({
469
- type: "assistant",
470
- message: {
471
- role: "assistant",
472
- content: [{ type: "text", text: "完整快照(与 delta 累计相同)" }],
473
- },
474
- timestamp_ms: 1778411927999,
475
- model_call_id: "mc-1",
476
- } as Parameters<typeof normalizeCursorMessage>[0]);
477
- expect(result).not.toBeNull();
478
- expect(result!.blocks).toEqual([
479
- { type: "text_final", text: "完整快照(与 delta 累计相同)" },
480
- ]);
481
- expect(result!.isFinalResponse).toBeUndefined();
482
- });
483
-
484
- it("Final flush(no timestamp_ms)→ text_final(覆盖)", () => {
485
- const result = normalizeCursorMessage({
486
- type: "assistant",
487
- message: {
488
- role: "assistant",
489
- content: [{ type: "text", text: "你上一题问的是 1+2=?" }],
490
- },
491
- });
492
- expect(result).not.toBeNull();
493
- expect(result!.blocks).toEqual([
494
- { type: "text_final", text: "你上一题问的是 1+2=?" },
495
- ]);
496
- expect(result!.isFinalResponse).toBeUndefined();
497
- });
498
- });
499
-
500
- describe("normalizeCursorMessage - result 消息(官方权威最终文本)", () => {
501
- it("result 消息提取 result 字段映射为 assistant + text_final", () => {
502
- const result = normalizeCursorMessage({
503
- type: "result",
504
- subtype: "success",
505
- result: "权威最终文本",
506
- });
507
- expect(result).not.toBeNull();
508
- expect(result!.type).toBe("assistant");
509
- expect(result!.blocks).toEqual([
510
- { type: "text_final", text: "权威最终文本" },
511
- ]);
512
- expect(result!.isFinalResponse).toBe(true);
513
- });
514
-
515
- it("result 消息没有 result 字段时返回 null(无可用文本)", () => {
516
- expect(
517
- normalizeCursorMessage({ type: "result", subtype: "error" }),
518
- ).toBeNull();
519
- });
520
-
521
- it("result 消息 result 字段为空字符串时返回 null", () => {
522
- expect(
523
- normalizeCursorMessage({ type: "result", subtype: "success", result: "" }),
524
- ).toBeNull();
525
- });
526
- });
527
-
528
- describe("Cursor stream fixture - 端到端不重复", () => {
529
- it("partial+final 流结束后最终回复只包含一段完整文本(不重复)", () => {
530
- const lines = readFixture("cursor_partial_with_final.jsonl");
531
- const state: AccumulatorState = {
532
- accumulatedContent: "",
533
- finalText: "",
534
- finalCompleteText: "",
535
- chunkCount: 0,
536
- };
537
- for (const raw of lines) {
538
- const normalized = normalizeCursorMessage(
539
- raw as Parameters<typeof normalizeCursorMessage>[0],
540
- );
541
- if (!normalized) continue;
542
- for (const block of normalized.blocks) {
543
- accumulateBlockContent(block, state);
544
- }
545
- }
546
-
547
- const expected = "你上一题问的是 **1+2=?**(一加二等于几)。";
548
- const reply = pickFinalReply(state);
549
- expect(reply).toBe(expected);
550
- // 关键断言:长度等于完整文本,不应翻倍
551
- expect(reply.length).toBe(expected.length);
552
- });
553
-
554
- it("仅 partial 无 final 时,回复 = partial 累加结果", () => {
555
- const lines = readFixture("cursor_partial_only.jsonl");
556
- const state: AccumulatorState = {
557
- accumulatedContent: "",
558
- finalText: "",
559
- finalCompleteText: "",
560
- chunkCount: 0,
561
- };
562
- for (const raw of lines) {
563
- const normalized = normalizeCursorMessage(
564
- raw as Parameters<typeof normalizeCursorMessage>[0],
565
- );
566
- if (!normalized) continue;
567
- for (const block of normalized.blocks) {
568
- accumulateBlockContent(block, state);
569
- }
570
- }
571
-
572
- // 注:fixture 末尾有 result 消息,按官方语义其 result 字段是权威最终文本
573
- expect(pickFinalReply(state)).toBe("hello world");
574
- });
575
-
576
- it("有工具调用回合(含 buffered flush)最终回复仍取 result.result,无重复", () => {
577
- const lines = readFixture("cursor_with_tool_call.jsonl");
578
- const state: AccumulatorState = {
579
- accumulatedContent: "",
580
- finalText: "",
581
- finalCompleteText: "",
582
- chunkCount: 0,
583
- };
584
- for (const raw of lines) {
585
- const normalized = normalizeCursorMessage(
586
- raw as Parameters<typeof normalizeCursorMessage>[0],
587
- );
588
- if (!normalized) continue;
589
- for (const block of normalized.blocks) {
590
- accumulateBlockContent(block, state);
591
- }
592
- }
593
-
594
- const expected = "我看下。两个文件。";
595
- const reply = pickFinalReply(state);
596
- expect(reply).toBe(expected);
597
- // 关键断言:buffered flush 不会让回复变长(不重复)
598
- expect(reply.length).toBe(expected.length);
599
- });
600
-
601
- it("buffered flush 之后的新增量文本不会被 pickFinalReply 吞掉", () => {
602
- // 模拟流中间状态:buffered flush 设置了 finalCompleteText,然后新的
603
- // 增量 text 到达。之前有个 bug 是 pickFinalReply 一直返回旧的
604
- // finalCompleteText,导致工具调用后的新文本在卡片中不可见。
605
- const state: AccumulatorState = {
606
- accumulatedContent: "",
607
- finalText: "",
608
- finalCompleteText: "",
609
- chunkCount: 0,
610
- };
611
-
612
- // 1) 工具调用前的增量(同 cursor_with_tool_call.jsonl lines 3-5)
613
- accumulateBlockContent({ type: "text", text: "我" }, state);
614
- accumulateBlockContent({ type: "text", text: "看下" }, state);
615
- accumulateBlockContent({ type: "text", text: "。" }, state);
616
- expect(state.finalText).toBe("我看下。");
617
-
618
- // 2) buffered flush 快照(含 tool_use,line 6)
619
- accumulateBlockContent({ type: "text_final", text: "我看下。" }, state);
620
- accumulateBlockContent({ type: "tool_use", name: "ls", input: { path: "." } }, state);
621
- expect(state.finalCompleteText).toBe("我看下。");
622
- // 此时 finalText 已经包含了与 flush 相同的文本
623
- expect(pickFinalReply(state)).toBe("我看下。");
624
-
625
- // 3) tool_result(line 7)
626
- accumulateBlockContent({ type: "tool_result", tool_use_id: "tool-1", content: "file1\nfile2", is_error: false }, state);
627
-
628
- // 4) 工具调用后的新增量(lines 8-9):关键断言
629
- accumulateBlockContent({ type: "text", text: "两" }, state);
630
- // 新的增量 text 应清空 finalCompleteText,pickFinalReply 回退到 finalText
631
- expect(state.finalCompleteText).toBe("");
632
- expect(state.finalText).toBe("我看下。两");
633
- expect(pickFinalReply(state)).toBe("我看下。两");
634
-
635
- accumulateBlockContent({ type: "text", text: "个文件" }, state);
636
- expect(pickFinalReply(state)).toBe("我看下。两个文件");
637
- });
638
-
639
- // -------------------------------------------------------------------------
640
- // 新格式:独立的 thinking 和 tool_call 消息(Cursor agent 实际发出的格式)
641
- // -------------------------------------------------------------------------
642
-
643
- it("normalizes independent thinking delta message", () => {
644
- const result = normalizeCursorMessage({
645
- type: "thinking",
646
- subtype: "delta",
647
- text: " Let me think...",
648
- session_id: "sid",
649
- timestamp_ms: 1000,
650
- } as Parameters<typeof normalizeCursorMessage>[0]);
651
- expect(result).not.toBeNull();
652
- expect(result!.blocks).toEqual([
653
- { type: "thinking", thinking: " Let me think..." },
654
- ]);
655
- });
656
-
657
- it("ignores thinking completed message", () => {
658
- const result = normalizeCursorMessage({
659
- type: "thinking",
660
- subtype: "completed",
661
- session_id: "sid",
662
- });
663
- expect(result).toBeNull();
664
- });
665
-
666
- it("normalizes tool_call started → tool_use block", () => {
667
- const result = normalizeCursorMessage({
668
- type: "tool_call",
669
- subtype: "started",
670
- call_id: "toolu_abc",
671
- tool_call: {
672
- shellToolCall: {
673
- args: { command: "ls" },
674
- description: "List files",
675
- },
676
- },
677
- session_id: "sid",
678
- timestamp_ms: 1000,
679
- } as Parameters<typeof normalizeCursorMessage>[0]);
680
- expect(result).not.toBeNull();
681
- expect(result!.blocks).toEqual([
682
- { type: "tool_use", id: "toolu_abc", name: "Bash", input: { command: "ls" } },
683
- ]);
684
- });
685
-
686
- it("normalizes tool_call completed → tool_result block (success)", () => {
687
- const result = normalizeCursorMessage({
688
- type: "tool_call",
689
- subtype: "completed",
690
- call_id: "toolu_abc",
691
- tool_call: {
692
- shellToolCall: {
693
- args: { command: "ls" },
694
- result: { success: { stdout: "file1\nfile2" } },
695
- },
696
- },
697
- session_id: "sid",
698
- } as Parameters<typeof normalizeCursorMessage>[0]);
699
- expect(result).not.toBeNull();
700
- expect(result!.blocks[0]).toMatchObject({
701
- type: "tool_result",
702
- tool_use_id: "toolu_abc",
703
- content: "file1\nfile2",
704
- is_error: undefined,
705
- });
706
- });
707
-
708
- it("normalizes tool_call completed → tool_result block (error)", () => {
709
- const result = normalizeCursorMessage({
710
- type: "tool_call",
711
- subtype: "completed",
712
- call_id: "toolu_err",
713
- tool_call: {
714
- readToolCall: {
715
- args: { filePath: "/nonexistent" },
716
- result: { error: "file not found" },
717
- },
718
- },
719
- } as Parameters<typeof normalizeCursorMessage>[0]);
720
- expect(result).not.toBeNull();
721
- expect(result!.blocks[0]).toMatchObject({
722
- type: "tool_result",
723
- tool_use_id: "toolu_err",
724
- is_error: true,
725
- });
726
- });
727
-
728
- it("mapToolCallKey: maps known keys to readable names", () => {
729
- const cases: [string, string][] = [
730
- ["globToolCall", "Glob"],
731
- ["shellToolCall", "Bash"],
732
- ["readToolCall", "Read"],
733
- ["grepToolCall", "Grep"],
734
- ["unknownCall", "unknownCall"],
735
- ];
736
- for (const [raw, expected] of cases) {
737
- const r = normalizeCursorMessage({
738
- type: "tool_call",
739
- subtype: "started",
740
- call_id: "id",
741
- tool_call: { [raw]: { args: {} } },
742
- } as Parameters<typeof normalizeCursorMessage>[0]);
743
- expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
744
- }
745
- });
746
- });
747
-
748
- describe("Cursor adapter process failures", () => {
749
- it("formats empty stdout auth stderr as a cautious visible message", () => {
750
- const message = formatCursorAgentEmptyOutputMessage({
751
- exitCode: 1,
752
- stdoutLength: 0,
753
- stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
754
- });
755
-
756
- expect(message).toContain("检测到认证相关错误");
757
- expect(message).toContain("可能需要重新登录 Cursor Agent");
758
- expect(message).toContain("CURSOR_API_KEY");
759
- expect(message).toContain("agent status");
760
- expect(message).toContain("agent login");
761
- expect(message).not.toContain("登录态已失效");
762
- });
763
-
764
- it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
765
- const message = formatCursorAgentEmptyOutputMessage({
766
- exitCode: 1,
767
- stdoutLength: 0,
768
- stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
769
- });
770
-
771
- expect(message).toContain("底层命令异常退出");
772
- expect(message).toContain("[Cursor stderr] exit=1");
773
- expect(message).toContain("ActionRequiredError");
774
- expect(message).toContain("unpaid invoice");
775
- expect(message).toContain("team administrator");
776
- });
777
-
778
- it("redacts obvious secrets from visible Cursor stderr", () => {
779
- const message = formatCursorAgentEmptyOutputMessage({
780
- exitCode: 1,
781
- stdoutLength: 0,
782
- stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
783
- });
784
-
785
- expect(message).toContain("token=<redacted>");
786
- expect(message).toContain("Bearer <redacted>");
787
- expect(message).not.toContain("secret-token-value");
788
- expect(message).not.toContain("abcdefghijklmnop");
789
- });
790
-
791
- it("does not format a message when stdout is non-empty", () => {
792
- expect(
793
- formatCursorAgentEmptyOutputMessage({
794
- exitCode: 1,
795
- stdoutLength: 10,
796
- stderr: "Error: Authentication required",
797
- }),
798
- ).toBeNull();
799
- });
800
-
801
- it("prompt surfaces auth-related empty output before failing the stream", async () => {
802
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
803
- const spawnImpl = (() =>
804
- createMockCursorProcess({
805
- exitCode: 1,
806
- stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
807
- })) as CursorSpawnForTest;
808
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
809
- const iterator = adapter.prompt(
810
- "sid",
811
- "[User message]\nhello\n[/User message]",
812
- "F:/repo",
813
- )[Symbol.asyncIterator]();
814
-
815
- const first = await iterator.next();
816
- expect(first.done).toBe(false);
817
- expect(first.value.blocks).toHaveLength(1);
818
- expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
819
- expect(first.value.blocks[0]).toHaveProperty(
820
- "text",
821
- expect.stringContaining("可能需要重新登录 Cursor Agent"),
822
- );
823
- expect(first.value.blocks[0]).toHaveProperty(
824
- "text",
825
- expect.not.stringContaining("登录态已失效"),
826
- );
827
-
828
- await expect(iterator.next()).rejects.toThrow(
829
- /Cursor Agent exited without stream-json output/,
830
- );
831
- });
832
-
833
- it("prompt surfaces action-required stderr before failing the stream", async () => {
834
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
835
- const spawnImpl = (() =>
836
- createMockCursorProcess({
837
- exitCode: 1,
838
- stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
839
- })) as CursorSpawnForTest;
840
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
841
- const iterator = adapter.prompt(
842
- "sid",
843
- "[User message]\nhello\n[/User message]",
844
- "F:/repo",
845
- )[Symbol.asyncIterator]();
846
-
847
- const first = await iterator.next();
848
- expect(first.done).toBe(false);
849
- expect(first.value.blocks).toHaveLength(1);
850
- expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
851
- expect(first.value.blocks[0]).toHaveProperty(
852
- "text",
853
- expect.stringContaining("ActionRequiredError"),
854
- );
855
- expect(first.value.blocks[0]).toHaveProperty(
856
- "text",
857
- expect.stringContaining("unpaid invoice"),
858
- );
859
-
860
- await expect(iterator.next()).rejects.toThrow(
861
- /Cursor Agent exited without stream-json output/,
862
- );
863
- });
864
-
865
- it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
866
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
867
- const spawnImpl = (() =>
868
- createHangingMockCursorProcess({
869
- stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
870
- })) as CursorSpawnForTest;
871
- const adapter = createCursorAdapter({
872
- metaStore: store,
873
- spawn: spawnImpl,
874
- badJsonIdleTimeoutMs: 10,
875
- });
876
-
877
- const pending = (async () => {
878
- for await (const _event of adapter.prompt(
879
- "sid",
880
- "[User message]\nhello\n[/User message]",
881
- "F:/repo",
882
- )) {
883
- // No normalized events are expected before the watchdog fires.
884
- }
885
- })().catch((error: unknown) => error);
886
-
887
- const error = await pending;
888
- expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
889
- });
890
- });