chatccc 0.2.252 → 0.2.253

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 (280) hide show
  1. package/README.md +1 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/deepccc-agent/os-prompts/darwin.md +8 -8
  5. package/deepccc-agent/os-prompts/linux.md +8 -8
  6. package/deepccc-agent/os-prompts/win32.md +11 -11
  7. package/deepccc-agent/package.json +65 -65
  8. package/dist/deepccc-agent/src/cli.js +634 -0
  9. package/dist/deepccc-agent/src/config.js +76 -0
  10. package/dist/deepccc-agent/src/context.js +348 -0
  11. package/dist/deepccc-agent/src/file-log.js +34 -0
  12. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  13. package/dist/deepccc-agent/src/index.js +571 -0
  14. package/dist/deepccc-agent/src/permissions.js +170 -0
  15. package/dist/deepccc-agent/src/privacy.js +124 -0
  16. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  17. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  18. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  19. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  20. package/dist/deepccc-agent/src/progress/view.js +30 -0
  21. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  22. package/dist/deepccc-agent/src/session-search.js +276 -0
  23. package/dist/deepccc-agent/src/session-select.js +23 -0
  24. package/dist/deepccc-agent/src/sigint.js +26 -0
  25. package/dist/deepccc-agent/src/skills.js +178 -0
  26. package/dist/deepccc-agent/src/web-tools.js +246 -0
  27. package/dist/src/adapters/adapter-interface.js +19 -0
  28. package/dist/src/adapters/ccc-adapter.js +112 -0
  29. package/dist/src/adapters/claude-adapter.js +497 -0
  30. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  31. package/dist/src/adapters/codex-adapter.js +279 -0
  32. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  33. package/dist/src/adapters/cursor-adapter.js +491 -0
  34. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  35. package/dist/src/adapters/jsonl-stream.js +104 -0
  36. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  37. package/dist/src/adapters/raw-stream-log.js +106 -0
  38. package/dist/src/adapters/resource-monitor.js +113 -0
  39. package/dist/src/agent-activity.js +133 -0
  40. package/dist/src/agent-delegate-task-rpc.js +129 -0
  41. package/dist/src/agent-delegate-task.js +48 -0
  42. package/dist/src/agent-file-rpc.js +152 -0
  43. package/dist/src/agent-image-rpc.js +148 -0
  44. package/dist/src/agent-platform-routing.js +13 -0
  45. package/dist/src/agent-reload-config-rpc.js +23 -0
  46. package/dist/src/agent-rpc-body.js +87 -0
  47. package/dist/src/agent-stop-stuck.js +110 -0
  48. package/dist/src/card-action-routing.js +7 -0
  49. package/dist/src/card-plain-text.js +101 -0
  50. package/dist/src/cardkit.js +158 -0
  51. package/dist/src/cards.js +573 -0
  52. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  53. package/dist/src/chatgpt-subscription.js +199 -0
  54. package/dist/src/chrome-devtools-guard.js +238 -0
  55. package/dist/src/claude-sdk-installer.js +249 -0
  56. package/dist/src/codex-reset-actions.js +143 -0
  57. package/dist/src/config-utils.js +149 -0
  58. package/dist/src/config.js +804 -0
  59. package/dist/src/cursor-usage.js +77 -0
  60. package/dist/src/exit-banner.js +28 -0
  61. package/dist/src/feishu-api.js +1404 -0
  62. package/dist/src/feishu-message-ingress.js +137 -0
  63. package/dist/src/feishu-platform.js +97 -0
  64. package/dist/src/format-message.js +252 -0
  65. package/dist/src/git-command.js +155 -0
  66. package/dist/src/im-skills.js +121 -0
  67. package/dist/src/index.js +833 -0
  68. package/dist/src/litellm-proxy.js +300 -0
  69. package/dist/src/orchestrator.js +2078 -0
  70. package/dist/src/package-root.js +26 -0
  71. package/dist/src/platform-adapter.js +7 -0
  72. package/dist/src/platform-startup.js +6 -0
  73. package/dist/src/privacy.js +100 -0
  74. package/dist/src/progress/reducer.js +102 -0
  75. package/dist/src/progress/terminal-renderer.js +264 -0
  76. package/dist/src/progress/view.js +30 -0
  77. package/dist/src/response-stall.js +14 -0
  78. package/dist/src/runtime-entry.js +13 -0
  79. package/dist/src/runtime-reload.js +19 -0
  80. package/dist/src/session-chat-binding.js +183 -0
  81. package/dist/src/session-name.js +7 -0
  82. package/dist/src/session.js +2144 -0
  83. package/dist/src/shared-prefix.js +16 -0
  84. package/dist/src/shared.js +493 -0
  85. package/dist/src/sim-agent.js +105 -0
  86. package/dist/src/sim-platform.js +142 -0
  87. package/dist/src/sim-store.js +231 -0
  88. package/dist/src/simplify.js +99 -0
  89. package/dist/src/startup-lifecycle.js +209 -0
  90. package/dist/src/stream-state.js +141 -0
  91. package/dist/src/terminal-error.js +100 -0
  92. package/dist/src/trace.js +50 -0
  93. package/dist/src/turn-cards.js +92 -0
  94. package/dist/src/update-command-guard.js +114 -0
  95. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  96. package/dist/src/wechat-platform.js +545 -0
  97. package/package.json +76 -74
  98. package/deepccc-agent/LICENSE +0 -201
  99. package/deepccc-agent/bin/deepccc.mjs +0 -26
  100. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  101. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  102. package/deepccc-agent/package-lock.json +0 -2027
  103. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -877
  104. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  105. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  106. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  107. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  108. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  109. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  110. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  111. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  112. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  113. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  114. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  115. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  116. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  117. package/deepccc-agent/src/cli.ts +0 -682
  118. package/deepccc-agent/src/config.ts +0 -101
  119. package/deepccc-agent/src/context.ts +0 -465
  120. package/deepccc-agent/src/file-log.ts +0 -38
  121. package/deepccc-agent/src/file-tools.ts +0 -1493
  122. package/deepccc-agent/src/index.ts +0 -710
  123. package/deepccc-agent/src/permissions.ts +0 -226
  124. package/deepccc-agent/src/privacy.ts +0 -141
  125. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  126. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  127. package/deepccc-agent/src/progress/reducer.ts +0 -113
  128. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  129. package/deepccc-agent/src/progress/view.ts +0 -77
  130. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  131. package/deepccc-agent/src/session-search.ts +0 -370
  132. package/deepccc-agent/src/session-select.ts +0 -48
  133. package/deepccc-agent/src/sigint.ts +0 -50
  134. package/deepccc-agent/src/skills.ts +0 -205
  135. package/deepccc-agent/src/web-tools.ts +0 -313
  136. package/deepccc-agent/tsconfig.build.json +0 -13
  137. package/deepccc-agent/tsconfig.json +0 -13
  138. package/deepccc-agent/vitest.config.ts +0 -7
  139. package/src/__tests__/adapter-interface.test.ts +0 -152
  140. package/src/__tests__/agent-activity.test.ts +0 -86
  141. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  142. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  143. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  144. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  145. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  146. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  147. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  148. package/src/__tests__/builtin-config.test.ts +0 -26
  149. package/src/__tests__/builtin-context.test.ts +0 -319
  150. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  151. package/src/__tests__/builtin-permissions.test.ts +0 -219
  152. package/src/__tests__/builtin-session-search.test.ts +0 -262
  153. package/src/__tests__/builtin-session-select.test.ts +0 -116
  154. package/src/__tests__/builtin-sigint.test.ts +0 -56
  155. package/src/__tests__/builtin-skills.test.ts +0 -284
  156. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  157. package/src/__tests__/card-action-routing.test.ts +0 -18
  158. package/src/__tests__/card-plain-text.test.ts +0 -45
  159. package/src/__tests__/cardkit.test.ts +0 -60
  160. package/src/__tests__/cards.test.ts +0 -607
  161. package/src/__tests__/ccc-adapter.test.ts +0 -194
  162. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  163. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  164. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  165. package/src/__tests__/claude-adapter.test.ts +0 -614
  166. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  167. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  168. package/src/__tests__/codex-adapter.test.ts +0 -331
  169. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  170. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  171. package/src/__tests__/config-reload.test.ts +0 -284
  172. package/src/__tests__/config-sample.test.ts +0 -97
  173. package/src/__tests__/config-utils.test.ts +0 -40
  174. package/src/__tests__/config.test.ts +0 -395
  175. package/src/__tests__/crash-logging.test.ts +0 -360
  176. package/src/__tests__/cursor-adapter.test.ts +0 -890
  177. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  178. package/src/__tests__/feishu-api.test.ts +0 -60
  179. package/src/__tests__/feishu-avatar.test.ts +0 -504
  180. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  181. package/src/__tests__/feishu-platform.test.ts +0 -75
  182. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  183. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  184. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  185. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  186. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  187. package/src/__tests__/format-message.test.ts +0 -316
  188. package/src/__tests__/git-command.test.ts +0 -288
  189. package/src/__tests__/im-skills.test.ts +0 -125
  190. package/src/__tests__/jsonl-stream.test.ts +0 -79
  191. package/src/__tests__/orchestrator.test.ts +0 -1268
  192. package/src/__tests__/package-files.test.ts +0 -24
  193. package/src/__tests__/platform-startup.test.ts +0 -19
  194. package/src/__tests__/privacy.test.ts +0 -198
  195. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  196. package/src/__tests__/progress-reducer.test.ts +0 -121
  197. package/src/__tests__/raw-stream-log.test.ts +0 -106
  198. package/src/__tests__/response-stall.test.ts +0 -49
  199. package/src/__tests__/restart.test.ts +0 -232
  200. package/src/__tests__/session-ccc-config.test.ts +0 -66
  201. package/src/__tests__/session.test.ts +0 -3004
  202. package/src/__tests__/shared-prefix.test.ts +0 -36
  203. package/src/__tests__/sim-agent.test.ts +0 -174
  204. package/src/__tests__/sim-platform.test.ts +0 -93
  205. package/src/__tests__/sim-store.test.ts +0 -214
  206. package/src/__tests__/simplify.test.ts +0 -283
  207. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  208. package/src/__tests__/stop-session.test.ts +0 -162
  209. package/src/__tests__/stream-state.test.ts +0 -164
  210. package/src/__tests__/terminal-error.test.ts +0 -54
  211. package/src/__tests__/terminal-renderer.test.ts +0 -247
  212. package/src/__tests__/update-command-guard.test.ts +0 -144
  213. package/src/__tests__/web-ui.test.ts +0 -438
  214. package/src/__tests__/wechat-platform.test.ts +0 -111
  215. package/src/adapters/adapter-interface.ts +0 -217
  216. package/src/adapters/ccc-adapter.ts +0 -150
  217. package/src/adapters/claude-adapter.ts +0 -673
  218. package/src/adapters/claude-session-meta-store.ts +0 -120
  219. package/src/adapters/codex-adapter.ts +0 -426
  220. package/src/adapters/codex-session-meta-store.ts +0 -131
  221. package/src/adapters/cursor-adapter.ts +0 -681
  222. package/src/adapters/cursor-session-meta-store.ts +0 -154
  223. package/src/adapters/jsonl-stream.ts +0 -157
  224. package/src/adapters/raw-stream-log.ts +0 -124
  225. package/src/adapters/resource-monitor.ts +0 -141
  226. package/src/agent-activity.ts +0 -175
  227. package/src/agent-delegate-task-rpc.ts +0 -153
  228. package/src/agent-delegate-task.ts +0 -91
  229. package/src/agent-file-rpc.ts +0 -172
  230. package/src/agent-image-rpc.ts +0 -168
  231. package/src/agent-platform-routing.ts +0 -28
  232. package/src/agent-reload-config-rpc.ts +0 -34
  233. package/src/agent-rpc-body.ts +0 -92
  234. package/src/agent-stop-stuck.ts +0 -129
  235. package/src/card-action-routing.ts +0 -14
  236. package/src/card-plain-text.ts +0 -108
  237. package/src/cardkit.ts +0 -179
  238. package/src/cards.ts +0 -684
  239. package/src/chatgpt-subscription-rpc.ts +0 -27
  240. package/src/chatgpt-subscription.ts +0 -299
  241. package/src/chrome-devtools-guard.ts +0 -318
  242. package/src/claude-sdk-installer.ts +0 -324
  243. package/src/codex-reset-actions.ts +0 -184
  244. package/src/config-utils.ts +0 -211
  245. package/src/config.ts +0 -1063
  246. package/src/cursor-usage.ts +0 -128
  247. package/src/exit-banner.ts +0 -33
  248. package/src/feishu-api.ts +0 -1616
  249. package/src/feishu-message-ingress.ts +0 -195
  250. package/src/feishu-platform.ts +0 -159
  251. package/src/format-message.ts +0 -293
  252. package/src/git-command.ts +0 -202
  253. package/src/im-skills.ts +0 -149
  254. package/src/index.ts +0 -1089
  255. package/src/litellm-proxy.ts +0 -374
  256. package/src/orchestrator.ts +0 -2543
  257. package/src/platform-adapter.ts +0 -70
  258. package/src/platform-startup.ts +0 -16
  259. package/src/privacy.ts +0 -118
  260. package/src/progress/reducer.ts +0 -113
  261. package/src/progress/terminal-renderer.ts +0 -294
  262. package/src/progress/view.ts +0 -77
  263. package/src/response-stall.ts +0 -28
  264. package/src/runtime-reload.ts +0 -34
  265. package/src/session-chat-binding.ts +0 -292
  266. package/src/session-name.ts +0 -8
  267. package/src/session.ts +0 -2659
  268. package/src/shared-prefix.ts +0 -29
  269. package/src/shared.ts +0 -552
  270. package/src/sim-agent.ts +0 -167
  271. package/src/sim-platform.ts +0 -177
  272. package/src/sim-store.ts +0 -317
  273. package/src/simplify.ts +0 -120
  274. package/src/startup-lifecycle.ts +0 -250
  275. package/src/stream-state.ts +0 -177
  276. package/src/terminal-error.ts +0 -129
  277. package/src/trace.ts +0 -51
  278. package/src/turn-cards.ts +0 -118
  279. package/src/update-command-guard.ts +0 -165
  280. 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
- });