chatccc 0.2.231 → 0.2.232

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 (127) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -86
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +449 -393
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +319 -202
  28. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  29. package/src/__tests__/builtin-permissions.test.ts +211 -211
  30. package/src/__tests__/builtin-session-search.test.ts +228 -0
  31. package/src/__tests__/builtin-session-select.test.ts +116 -116
  32. package/src/__tests__/builtin-sigint.test.ts +56 -56
  33. package/src/__tests__/builtin-skills.test.ts +284 -284
  34. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  35. package/src/__tests__/card-action-routing.test.ts +18 -18
  36. package/src/__tests__/cardkit.test.ts +60 -60
  37. package/src/__tests__/ccc-adapter.test.ts +170 -170
  38. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  39. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  40. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  41. package/src/__tests__/claude-adapter.test.ts +614 -614
  42. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  43. package/src/__tests__/codex-adapter.test.ts +58 -58
  44. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  45. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  46. package/src/__tests__/cursor-adapter.test.ts +268 -268
  47. package/src/__tests__/feishu-api.test.ts +60 -60
  48. package/src/__tests__/feishu-avatar.test.ts +164 -164
  49. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  50. package/src/__tests__/feishu-platform.test.ts +22 -22
  51. package/src/__tests__/format-message.test.ts +47 -47
  52. package/src/__tests__/jsonl-stream.test.ts +79 -79
  53. package/src/__tests__/package-files.test.ts +24 -24
  54. package/src/__tests__/privacy.test.ts +198 -198
  55. package/src/__tests__/progress-reducer.test.ts +121 -121
  56. package/src/__tests__/raw-stream-log.test.ts +106 -106
  57. package/src/__tests__/response-stall.test.ts +49 -49
  58. package/src/__tests__/restart.test.ts +132 -132
  59. package/src/__tests__/session.test.ts +199 -199
  60. package/src/__tests__/shared-prefix.test.ts +36 -36
  61. package/src/__tests__/sim-platform.test.ts +16 -16
  62. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  63. package/src/__tests__/stop-session.test.ts +34 -34
  64. package/src/__tests__/stream-state.test.ts +42 -42
  65. package/src/__tests__/terminal-error.test.ts +54 -54
  66. package/src/__tests__/terminal-renderer.test.ts +247 -247
  67. package/src/__tests__/update-command-guard.test.ts +144 -144
  68. package/src/__tests__/web-ui.test.ts +326 -326
  69. package/src/adapters/adapter-interface.ts +33 -33
  70. package/src/adapters/ccc-adapter.ts +141 -141
  71. package/src/adapters/claude-adapter.ts +620 -620
  72. package/src/adapters/claude-session-meta-store.ts +120 -120
  73. package/src/adapters/codex-adapter.ts +426 -426
  74. package/src/adapters/cursor-adapter.ts +681 -681
  75. package/src/adapters/jsonl-stream.ts +157 -157
  76. package/src/adapters/raw-stream-log.ts +124 -124
  77. package/src/adapters/resource-monitor.ts +140 -140
  78. package/src/agent-activity.ts +175 -175
  79. package/src/agent-delegate-task-rpc.ts +153 -153
  80. package/src/agent-delegate-task.ts +91 -91
  81. package/src/agent-reload-config-rpc.ts +34 -34
  82. package/src/agent-stop-stuck.ts +129 -129
  83. package/src/builtin/cli.ts +6 -0
  84. package/src/builtin/config.ts +84 -84
  85. package/src/builtin/context.ts +465 -391
  86. package/src/builtin/file-log.ts +38 -38
  87. package/src/builtin/file-tools.ts +34 -0
  88. package/src/builtin/index.ts +24 -23
  89. package/src/builtin/proc-tree-kill.ts +61 -61
  90. package/src/builtin/progress/cards-helpers.ts +76 -76
  91. package/src/builtin/progress/reducer.ts +113 -113
  92. package/src/builtin/progress/terminal-renderer.ts +294 -294
  93. package/src/builtin/progress/view.ts +77 -77
  94. package/src/builtin/raw-stream-log.ts +124 -124
  95. package/src/builtin/session-search.ts +363 -0
  96. package/src/builtin/session-select.ts +48 -48
  97. package/src/builtin/sigint.ts +50 -50
  98. package/src/builtin/skills.ts +205 -205
  99. package/src/builtin/web-tools.ts +313 -313
  100. package/src/card-action-routing.ts +14 -14
  101. package/src/chatgpt-subscription-rpc.ts +27 -27
  102. package/src/chatgpt-subscription.ts +299 -299
  103. package/src/chrome-devtools-guard.ts +318 -318
  104. package/src/codex-reset-actions.ts +184 -184
  105. package/src/feishu-api.ts +193 -193
  106. package/src/feishu-message-ingress.ts +195 -195
  107. package/src/feishu-platform.ts +20 -20
  108. package/src/format-message.ts +293 -293
  109. package/src/litellm-proxy.ts +374 -374
  110. package/src/orchestrator.ts +2494 -2494
  111. package/src/platform-adapter.ts +6 -6
  112. package/src/privacy.ts +118 -118
  113. package/src/progress/reducer.ts +113 -113
  114. package/src/progress/terminal-renderer.ts +294 -294
  115. package/src/progress/view.ts +77 -77
  116. package/src/response-stall.ts +28 -28
  117. package/src/runtime-reload.ts +34 -34
  118. package/src/session-chat-binding.ts +82 -82
  119. package/src/session-name.ts +8 -8
  120. package/src/session.ts +201 -201
  121. package/src/shared-prefix.ts +29 -29
  122. package/src/sim-platform.ts +20 -20
  123. package/src/startup-lifecycle.ts +250 -250
  124. package/src/stream-state.ts +21 -21
  125. package/src/terminal-error.ts +129 -129
  126. package/src/turn-cards.ts +117 -117
  127. package/src/update-command-guard.ts +165 -165
@@ -1,326 +1,326 @@
1
- import { createServer } from "node:http";
2
- import type { AddressInfo } from "node:net";
3
- import { describe, it, expect } from "vitest";
4
- import {
5
- AGENT_TEAM_PAGE_HTML,
6
- PAGE_HTML,
7
- chooseStartPath,
8
- createUiRouter,
9
- getRestartRequiredReasons,
10
- unflattenConfig,
11
- } from "../web-ui.ts";
12
-
13
- describe("Agent Team page", () => {
14
- it("adds a prominent global entry to the ChatCCC page", () => {
15
- expect(PAGE_HTML).toContain('href="/agent-team"');
16
- expect(PAGE_HTML).toContain('class="agent-team-entry"');
17
- expect(PAGE_HTML).toContain("Agent Team");
18
- expect(PAGE_HTML).toContain("linear-gradient");
19
- });
20
-
21
- it("contains only the Agent Team title as page content", () => {
22
- expect(AGENT_TEAM_PAGE_HTML).toContain("<title>Agent Team</title>");
23
- expect(AGENT_TEAM_PAGE_HTML).toContain("<h1>Agent Team</h1>");
24
- expect(AGENT_TEAM_PAGE_HTML).not.toContain("dashboard-view");
25
- expect(AGENT_TEAM_PAGE_HTML).not.toContain("wizard-view");
26
- });
27
-
28
- it("serves the Agent Team page from its own route", async () => {
29
- const server = createServer(createUiRouter());
30
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
31
- const port = (server.address() as AddressInfo).port;
32
- try {
33
- const response = await fetch(`http://127.0.0.1:${port}/agent-team`);
34
- expect(response.status).toBe(200);
35
- expect(await response.text()).toBe(AGENT_TEAM_PAGE_HTML);
36
- } finally {
37
- await new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
38
- }
39
- });
40
- });
41
-
42
- describe("unflattenConfig", () => {
43
- it("maps Claude subagent model into claude.subagentModel", () => {
44
- expect(
45
- unflattenConfig({
46
- CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
47
- CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
48
- }),
49
- ).toEqual({
50
- claude: {
51
- model: "claude-sonnet-4-6",
52
- subagentModel: "claude-haiku-4-5-20251001",
53
- },
54
- });
55
- });
56
-
57
- it("maps Claude apiKey and baseUrl into claude config", () => {
58
- expect(
59
- unflattenConfig({
60
- CHATCCC_ANTHROPIC_API_KEY: "sk-test-key",
61
- CHATCCC_ANTHROPIC_BASE_URL: "https://api.example.com",
62
- }),
63
- ).toEqual({
64
- claude: {
65
- apiKey: "sk-test-key",
66
- baseUrl: "https://api.example.com",
67
- },
68
- });
69
- });
70
-
71
- it("maps Cursor and Codex runtime settings into agent config", () => {
72
- expect(
73
- unflattenConfig({
74
- CHATCCC_CURSOR_ALTERNATIVE_MODEL: "gpt-5.5-high",
75
- CHATCCC_CODEX_ALTERNATIVE_MODEL: "gpt-5.3-codex",
76
- CHATCCC_CODEX_FAST_MODE: true,
77
- }),
78
- ).toEqual({
79
- cursor: {
80
- alternativeModel: "gpt-5.5-high",
81
- },
82
- codex: {
83
- alternativeModel: "gpt-5.3-codex",
84
- fastMode: true,
85
- },
86
- });
87
- });
88
-
89
- it("maps CCC Agent settings into ccc config", () => {
90
- expect(
91
- unflattenConfig({
92
- CHATCCC_CCC_ENABLED: true,
93
- CHATCCC_CCC_DEFAULT_AGENT: true,
94
- CHATCCC_CCC_API_KEY: "sk-test-ccc",
95
- CHATCCC_CCC_BASE_URL: "https://api.deepseek.com/v1",
96
- CHATCCC_CCC_MODEL: "deepseek-v4-flash",
97
- CHATCCC_CCC_ALTERNATIVE_MODEL: "deepseek-v4-pro",
98
- CHATCCC_CCC_EFFORT: "max",
99
- }),
100
- ).toEqual({
101
- ccc: {
102
- enabled: true,
103
- defaultAgent: true,
104
- DEEPSEEK_API_KEY: "sk-test-ccc",
105
- DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
106
- model: "deepseek-v4-flash",
107
- alternativeModel: "deepseek-v4-pro",
108
- effort: "max",
109
- },
110
- });
111
- });
112
-
113
- it("maps Chrome CDP guard fields into chromeDevtools config", () => {
114
- expect(
115
- unflattenConfig({
116
- CHATCCC_CHROME_DEVTOOLS_ENABLED: true,
117
- CHATCCC_CHROME_DEVTOOLS_PORT: "15166",
118
- CHATCCC_CHROME_DEVTOOLS_PATH: "C:/Program Files/Google/Chrome/Application/chrome.exe",
119
- }),
120
- ).toEqual({
121
- chromeDevtools: {
122
- enabled: true,
123
- port: 15166,
124
- chromePath: "C:/Program Files/Google/Chrome/Application/chrome.exe",
125
- },
126
- });
127
- });
128
-
129
- it("maps the Web UI startup preference into webUi config", () => {
130
- expect(
131
- unflattenConfig({
132
- CHATCCC_WEB_UI_OPEN_ON_START: false,
133
- }),
134
- ).toEqual({
135
- webUi: {
136
- openOnStart: false,
137
- },
138
- });
139
- });
140
- });
141
-
142
- describe("getRestartRequiredReasons", () => {
143
- const baseConfig = {
144
- feishu: { appId: "cli_old", appSecret: "secret_old" },
145
- platforms: {
146
- feishu: { enabled: true, platformType: "feishu" },
147
- ilink: { enabled: true, reuseTokenOnStart: true },
148
- },
149
- webUi: { openOnStart: true },
150
- chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
151
- port: 18080,
152
- claude: {
153
- enabled: true,
154
- defaultAgent: true,
155
- model: "",
156
- subagentModel: "",
157
- effort: "",
158
- apiKey: "",
159
- baseUrl: "",
160
- maxTurn: 0,
161
- },
162
- cursor: {
163
- enabled: true,
164
- defaultAgent: false,
165
- path: "",
166
- model: "",
167
- alternativeModel: "",
168
- avatarBatteryMode: "apiPercent",
169
- onDemandMonthlyBudget: 1000,
170
- },
171
- codex: {
172
- enabled: true,
173
- defaultAgent: false,
174
- path: "",
175
- model: "",
176
- alternativeModel: "",
177
- effort: "",
178
- fastMode: false,
179
- },
180
- ccc: {
181
- enabled: true,
182
- defaultAgent: false,
183
- DEEPSEEK_API_KEY: "sk-test-ccc",
184
- DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
185
- model: "deepseek-v4-pro",
186
- alternativeModel: "",
187
- },
188
- };
189
-
190
- it("does not require restart for agent and Chrome runtime settings", () => {
191
- expect(
192
- getRestartRequiredReasons(baseConfig, {
193
- ...baseConfig,
194
- chromeDevtools: { enabled: true, port: 15167, chromePath: "C:/Chrome/chrome.exe" },
195
- claude: { ...baseConfig.claude, model: "claude-sonnet", apiKey: "sk-test", maxTurn: 8 },
196
- cursor: { ...baseConfig.cursor, path: "C:/cursor-agent.cmd", model: "cursor-model" },
197
- codex: {
198
- ...baseConfig.codex,
199
- path: "C:/codex.cmd",
200
- model: "gpt-5.3-codex",
201
- effort: "high",
202
- fastMode: true,
203
- },
204
- }),
205
- ).toEqual([]);
206
- });
207
-
208
- it("requires restart for port, Feishu credentials, platform type, and platform lifecycle", () => {
209
- expect(
210
- getRestartRequiredReasons(baseConfig, {
211
- ...baseConfig,
212
- feishu: { appId: "cli_new", appSecret: "secret_new" },
213
- platforms: {
214
- feishu: { enabled: false, platformType: "lark" },
215
- ilink: { enabled: false, reuseTokenOnStart: false },
216
- },
217
- port: 18081,
218
- }),
219
- ).toEqual([
220
- "port",
221
- "feishu.appId",
222
- "feishu.appSecret",
223
- "platforms.feishu.platformType",
224
- "platforms.feishu.enabled",
225
- "platforms.ilink.enabled",
226
- "platforms.ilink.reuseTokenOnStart",
227
- ]);
228
- });
229
- });
230
-
231
- describe("dashboard edit modal", () => {
232
- it("keeps the embedded dashboard script syntactically valid", () => {
233
- const script = PAGE_HTML.match(/<script>([\s\S]*)<\/script>/)?.[1];
234
- expect(script).toBeTruthy();
235
- expect(() => new Function(script ?? "")).not.toThrow();
236
- });
237
-
238
- it("shows the edit modal and overlay when a section edit button is clicked", () => {
239
- expect(PAGE_HTML).toContain("function editSection(section)");
240
- expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
241
- expect(PAGE_HTML).toContain("document.getElementById('edit-overlay').classList.remove('hidden');");
242
- });
243
-
244
- it("uses plain alternative model labels for Cursor and Codex", () => {
245
- expect(PAGE_HTML).toContain("field-CHATCCC_CURSOR_ALTERNATIVE_MODEL");
246
- expect(PAGE_HTML).toContain("field-CHATCCC_CODEX_ALTERNATIVE_MODEL");
247
- expect(PAGE_HTML).toContain("备选模型");
248
- });
249
-
250
- it("shows CCC Agent in the wizard and dashboard with model switching fields", () => {
251
- expect(PAGE_HTML).toContain('id="agent-card-ccc"');
252
- expect(PAGE_HTML).toContain('id="agent-enable-ccc"');
253
- expect(PAGE_HTML).toContain('id="agent-default-ccc"');
254
- expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_MODEL"');
255
- expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_ALTERNATIVE_MODEL"');
256
- expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_EFFORT"');
257
- expect(PAGE_HTML).toContain('id="dash-ccc"');
258
- expect(PAGE_HTML).toContain("editSection('ccc')");
259
- });
260
-
261
- it("shows config effect scope hints", () => {
262
- expect(PAGE_HTML).toContain("生效范围:保存后下一条消息或下个新会话生效");
263
- expect(PAGE_HTML).toContain("生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC");
264
- });
265
-
266
- it("shows the Codex Fast mode switch in both the wizard and dashboard", () => {
267
- expect(PAGE_HTML).toContain('id="field-CHATCCC_CODEX_FAST_MODE"');
268
- expect(PAGE_HTML).toContain('id="cfg-CODEX_FAST_MODE"');
269
- expect(PAGE_HTML).toContain("Fast 模式");
270
- });
271
-
272
- it("shows the Web UI startup switch in both the wizard and dashboard", () => {
273
- expect(PAGE_HTML).toContain('id="field-CHATCCC_WEB_UI_OPEN_ON_START"');
274
- expect(PAGE_HTML).toContain('id="cfg-WEB_UI_OPEN_ON_START"');
275
- expect(PAGE_HTML).toContain("editSection('webUi')");
276
- expect(PAGE_HTML).toContain("下次直接启动生效;内部重启始终不打开");
277
- });
278
- });
279
-
280
- // ---------------------------------------------------------------------------
281
- // chooseStartPath — /api/start 的路径选择
282
- // 关键护栏:
283
- // - setup 模式(hasInplaceActivateHook=true)下 isServiceRunning 永远为 true
284
- // (setup 进程自己占着 PID 文件),必须无条件走 inplace;否则用户点
285
- // "保存并启动"将永远拿到 "Service is already running"。
286
- // - dashboard 模式 + service 已运行(通常就是当前进程自己)→ "reload":
287
- // 用户点"保存并启动"想让新 config 生效,但服务正在跑——不真重启,仅
288
- // 调用 reloadConfigFromDisk() 刷新进程内 export let 常量。绝不能再
289
- // 返回"already running"挡用户路。
290
- // - dashboard 模式 + service 未运行 → spawn 一个新的(旧 service 退出后场景)。
291
- // ---------------------------------------------------------------------------
292
-
293
- describe("chooseStartPath", () => {
294
- it("setup 模式(注入 inplace hook)→ inplace(不管 PID 文件状态)", () => {
295
- expect(
296
- chooseStartPath({
297
- hasInplaceActivateHook: true,
298
- isServiceRunning: true,
299
- }),
300
- ).toBe("inplace");
301
- expect(
302
- chooseStartPath({
303
- hasInplaceActivateHook: true,
304
- isServiceRunning: false,
305
- }),
306
- ).toBe("inplace");
307
- });
308
-
309
- it("dashboard 模式 + service 已运行 → reload(仅刷新 config,不真重启)", () => {
310
- expect(
311
- chooseStartPath({
312
- hasInplaceActivateHook: false,
313
- isServiceRunning: true,
314
- }),
315
- ).toBe("reload");
316
- });
317
-
318
- it("dashboard 模式 + service 未运行 → spawn", () => {
319
- expect(
320
- chooseStartPath({
321
- hasInplaceActivateHook: false,
322
- isServiceRunning: false,
323
- }),
324
- ).toBe("spawn");
325
- });
326
- });
1
+ import { createServer } from "node:http";
2
+ import type { AddressInfo } from "node:net";
3
+ import { describe, it, expect } from "vitest";
4
+ import {
5
+ AGENT_TEAM_PAGE_HTML,
6
+ PAGE_HTML,
7
+ chooseStartPath,
8
+ createUiRouter,
9
+ getRestartRequiredReasons,
10
+ unflattenConfig,
11
+ } from "../web-ui.ts";
12
+
13
+ describe("Agent Team page", () => {
14
+ it("adds a prominent global entry to the ChatCCC page", () => {
15
+ expect(PAGE_HTML).toContain('href="/agent-team"');
16
+ expect(PAGE_HTML).toContain('class="agent-team-entry"');
17
+ expect(PAGE_HTML).toContain("Agent Team");
18
+ expect(PAGE_HTML).toContain("linear-gradient");
19
+ });
20
+
21
+ it("contains only the Agent Team title as page content", () => {
22
+ expect(AGENT_TEAM_PAGE_HTML).toContain("<title>Agent Team</title>");
23
+ expect(AGENT_TEAM_PAGE_HTML).toContain("<h1>Agent Team</h1>");
24
+ expect(AGENT_TEAM_PAGE_HTML).not.toContain("dashboard-view");
25
+ expect(AGENT_TEAM_PAGE_HTML).not.toContain("wizard-view");
26
+ });
27
+
28
+ it("serves the Agent Team page from its own route", async () => {
29
+ const server = createServer(createUiRouter());
30
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
31
+ const port = (server.address() as AddressInfo).port;
32
+ try {
33
+ const response = await fetch(`http://127.0.0.1:${port}/agent-team`);
34
+ expect(response.status).toBe(200);
35
+ expect(await response.text()).toBe(AGENT_TEAM_PAGE_HTML);
36
+ } finally {
37
+ await new Promise<void>((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
38
+ }
39
+ });
40
+ });
41
+
42
+ describe("unflattenConfig", () => {
43
+ it("maps Claude subagent model into claude.subagentModel", () => {
44
+ expect(
45
+ unflattenConfig({
46
+ CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
47
+ CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
48
+ }),
49
+ ).toEqual({
50
+ claude: {
51
+ model: "claude-sonnet-4-6",
52
+ subagentModel: "claude-haiku-4-5-20251001",
53
+ },
54
+ });
55
+ });
56
+
57
+ it("maps Claude apiKey and baseUrl into claude config", () => {
58
+ expect(
59
+ unflattenConfig({
60
+ CHATCCC_ANTHROPIC_API_KEY: "sk-test-key",
61
+ CHATCCC_ANTHROPIC_BASE_URL: "https://api.example.com",
62
+ }),
63
+ ).toEqual({
64
+ claude: {
65
+ apiKey: "sk-test-key",
66
+ baseUrl: "https://api.example.com",
67
+ },
68
+ });
69
+ });
70
+
71
+ it("maps Cursor and Codex runtime settings into agent config", () => {
72
+ expect(
73
+ unflattenConfig({
74
+ CHATCCC_CURSOR_ALTERNATIVE_MODEL: "gpt-5.5-high",
75
+ CHATCCC_CODEX_ALTERNATIVE_MODEL: "gpt-5.3-codex",
76
+ CHATCCC_CODEX_FAST_MODE: true,
77
+ }),
78
+ ).toEqual({
79
+ cursor: {
80
+ alternativeModel: "gpt-5.5-high",
81
+ },
82
+ codex: {
83
+ alternativeModel: "gpt-5.3-codex",
84
+ fastMode: true,
85
+ },
86
+ });
87
+ });
88
+
89
+ it("maps CCC Agent settings into ccc config", () => {
90
+ expect(
91
+ unflattenConfig({
92
+ CHATCCC_CCC_ENABLED: true,
93
+ CHATCCC_CCC_DEFAULT_AGENT: true,
94
+ CHATCCC_CCC_API_KEY: "sk-test-ccc",
95
+ CHATCCC_CCC_BASE_URL: "https://api.deepseek.com/v1",
96
+ CHATCCC_CCC_MODEL: "deepseek-v4-flash",
97
+ CHATCCC_CCC_ALTERNATIVE_MODEL: "deepseek-v4-pro",
98
+ CHATCCC_CCC_EFFORT: "max",
99
+ }),
100
+ ).toEqual({
101
+ ccc: {
102
+ enabled: true,
103
+ defaultAgent: true,
104
+ DEEPSEEK_API_KEY: "sk-test-ccc",
105
+ DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
106
+ model: "deepseek-v4-flash",
107
+ alternativeModel: "deepseek-v4-pro",
108
+ effort: "max",
109
+ },
110
+ });
111
+ });
112
+
113
+ it("maps Chrome CDP guard fields into chromeDevtools config", () => {
114
+ expect(
115
+ unflattenConfig({
116
+ CHATCCC_CHROME_DEVTOOLS_ENABLED: true,
117
+ CHATCCC_CHROME_DEVTOOLS_PORT: "15166",
118
+ CHATCCC_CHROME_DEVTOOLS_PATH: "C:/Program Files/Google/Chrome/Application/chrome.exe",
119
+ }),
120
+ ).toEqual({
121
+ chromeDevtools: {
122
+ enabled: true,
123
+ port: 15166,
124
+ chromePath: "C:/Program Files/Google/Chrome/Application/chrome.exe",
125
+ },
126
+ });
127
+ });
128
+
129
+ it("maps the Web UI startup preference into webUi config", () => {
130
+ expect(
131
+ unflattenConfig({
132
+ CHATCCC_WEB_UI_OPEN_ON_START: false,
133
+ }),
134
+ ).toEqual({
135
+ webUi: {
136
+ openOnStart: false,
137
+ },
138
+ });
139
+ });
140
+ });
141
+
142
+ describe("getRestartRequiredReasons", () => {
143
+ const baseConfig = {
144
+ feishu: { appId: "cli_old", appSecret: "secret_old" },
145
+ platforms: {
146
+ feishu: { enabled: true, platformType: "feishu" },
147
+ ilink: { enabled: true, reuseTokenOnStart: true },
148
+ },
149
+ webUi: { openOnStart: true },
150
+ chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
151
+ port: 18080,
152
+ claude: {
153
+ enabled: true,
154
+ defaultAgent: true,
155
+ model: "",
156
+ subagentModel: "",
157
+ effort: "",
158
+ apiKey: "",
159
+ baseUrl: "",
160
+ maxTurn: 0,
161
+ },
162
+ cursor: {
163
+ enabled: true,
164
+ defaultAgent: false,
165
+ path: "",
166
+ model: "",
167
+ alternativeModel: "",
168
+ avatarBatteryMode: "apiPercent",
169
+ onDemandMonthlyBudget: 1000,
170
+ },
171
+ codex: {
172
+ enabled: true,
173
+ defaultAgent: false,
174
+ path: "",
175
+ model: "",
176
+ alternativeModel: "",
177
+ effort: "",
178
+ fastMode: false,
179
+ },
180
+ ccc: {
181
+ enabled: true,
182
+ defaultAgent: false,
183
+ DEEPSEEK_API_KEY: "sk-test-ccc",
184
+ DEEPSEEK_BASE_URL: "https://api.deepseek.com/v1",
185
+ model: "deepseek-v4-pro",
186
+ alternativeModel: "",
187
+ },
188
+ };
189
+
190
+ it("does not require restart for agent and Chrome runtime settings", () => {
191
+ expect(
192
+ getRestartRequiredReasons(baseConfig, {
193
+ ...baseConfig,
194
+ chromeDevtools: { enabled: true, port: 15167, chromePath: "C:/Chrome/chrome.exe" },
195
+ claude: { ...baseConfig.claude, model: "claude-sonnet", apiKey: "sk-test", maxTurn: 8 },
196
+ cursor: { ...baseConfig.cursor, path: "C:/cursor-agent.cmd", model: "cursor-model" },
197
+ codex: {
198
+ ...baseConfig.codex,
199
+ path: "C:/codex.cmd",
200
+ model: "gpt-5.3-codex",
201
+ effort: "high",
202
+ fastMode: true,
203
+ },
204
+ }),
205
+ ).toEqual([]);
206
+ });
207
+
208
+ it("requires restart for port, Feishu credentials, platform type, and platform lifecycle", () => {
209
+ expect(
210
+ getRestartRequiredReasons(baseConfig, {
211
+ ...baseConfig,
212
+ feishu: { appId: "cli_new", appSecret: "secret_new" },
213
+ platforms: {
214
+ feishu: { enabled: false, platformType: "lark" },
215
+ ilink: { enabled: false, reuseTokenOnStart: false },
216
+ },
217
+ port: 18081,
218
+ }),
219
+ ).toEqual([
220
+ "port",
221
+ "feishu.appId",
222
+ "feishu.appSecret",
223
+ "platforms.feishu.platformType",
224
+ "platforms.feishu.enabled",
225
+ "platforms.ilink.enabled",
226
+ "platforms.ilink.reuseTokenOnStart",
227
+ ]);
228
+ });
229
+ });
230
+
231
+ describe("dashboard edit modal", () => {
232
+ it("keeps the embedded dashboard script syntactically valid", () => {
233
+ const script = PAGE_HTML.match(/<script>([\s\S]*)<\/script>/)?.[1];
234
+ expect(script).toBeTruthy();
235
+ expect(() => new Function(script ?? "")).not.toThrow();
236
+ });
237
+
238
+ it("shows the edit modal and overlay when a section edit button is clicked", () => {
239
+ expect(PAGE_HTML).toContain("function editSection(section)");
240
+ expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
241
+ expect(PAGE_HTML).toContain("document.getElementById('edit-overlay').classList.remove('hidden');");
242
+ });
243
+
244
+ it("uses plain alternative model labels for Cursor and Codex", () => {
245
+ expect(PAGE_HTML).toContain("field-CHATCCC_CURSOR_ALTERNATIVE_MODEL");
246
+ expect(PAGE_HTML).toContain("field-CHATCCC_CODEX_ALTERNATIVE_MODEL");
247
+ expect(PAGE_HTML).toContain("备选模型");
248
+ });
249
+
250
+ it("shows CCC Agent in the wizard and dashboard with model switching fields", () => {
251
+ expect(PAGE_HTML).toContain('id="agent-card-ccc"');
252
+ expect(PAGE_HTML).toContain('id="agent-enable-ccc"');
253
+ expect(PAGE_HTML).toContain('id="agent-default-ccc"');
254
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_MODEL"');
255
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_ALTERNATIVE_MODEL"');
256
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_CCC_EFFORT"');
257
+ expect(PAGE_HTML).toContain('id="dash-ccc"');
258
+ expect(PAGE_HTML).toContain("editSection('ccc')");
259
+ });
260
+
261
+ it("shows config effect scope hints", () => {
262
+ expect(PAGE_HTML).toContain("生效范围:保存后下一条消息或下个新会话生效");
263
+ expect(PAGE_HTML).toContain("生效范围:飞书开关、App ID、App Secret 或平台类型变更需要重启 ChatCCC");
264
+ });
265
+
266
+ it("shows the Codex Fast mode switch in both the wizard and dashboard", () => {
267
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_CODEX_FAST_MODE"');
268
+ expect(PAGE_HTML).toContain('id="cfg-CODEX_FAST_MODE"');
269
+ expect(PAGE_HTML).toContain("Fast 模式");
270
+ });
271
+
272
+ it("shows the Web UI startup switch in both the wizard and dashboard", () => {
273
+ expect(PAGE_HTML).toContain('id="field-CHATCCC_WEB_UI_OPEN_ON_START"');
274
+ expect(PAGE_HTML).toContain('id="cfg-WEB_UI_OPEN_ON_START"');
275
+ expect(PAGE_HTML).toContain("editSection('webUi')");
276
+ expect(PAGE_HTML).toContain("下次直接启动生效;内部重启始终不打开");
277
+ });
278
+ });
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // chooseStartPath — /api/start 的路径选择
282
+ // 关键护栏:
283
+ // - setup 模式(hasInplaceActivateHook=true)下 isServiceRunning 永远为 true
284
+ // (setup 进程自己占着 PID 文件),必须无条件走 inplace;否则用户点
285
+ // "保存并启动"将永远拿到 "Service is already running"。
286
+ // - dashboard 模式 + service 已运行(通常就是当前进程自己)→ "reload":
287
+ // 用户点"保存并启动"想让新 config 生效,但服务正在跑——不真重启,仅
288
+ // 调用 reloadConfigFromDisk() 刷新进程内 export let 常量。绝不能再
289
+ // 返回"already running"挡用户路。
290
+ // - dashboard 模式 + service 未运行 → spawn 一个新的(旧 service 退出后场景)。
291
+ // ---------------------------------------------------------------------------
292
+
293
+ describe("chooseStartPath", () => {
294
+ it("setup 模式(注入 inplace hook)→ inplace(不管 PID 文件状态)", () => {
295
+ expect(
296
+ chooseStartPath({
297
+ hasInplaceActivateHook: true,
298
+ isServiceRunning: true,
299
+ }),
300
+ ).toBe("inplace");
301
+ expect(
302
+ chooseStartPath({
303
+ hasInplaceActivateHook: true,
304
+ isServiceRunning: false,
305
+ }),
306
+ ).toBe("inplace");
307
+ });
308
+
309
+ it("dashboard 模式 + service 已运行 → reload(仅刷新 config,不真重启)", () => {
310
+ expect(
311
+ chooseStartPath({
312
+ hasInplaceActivateHook: false,
313
+ isServiceRunning: true,
314
+ }),
315
+ ).toBe("reload");
316
+ });
317
+
318
+ it("dashboard 模式 + service 未运行 → spawn", () => {
319
+ expect(
320
+ chooseStartPath({
321
+ hasInplaceActivateHook: false,
322
+ isServiceRunning: false,
323
+ }),
324
+ ).toBe("spawn");
325
+ });
326
+ });