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