chatccc 0.2.198 → 0.2.199

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 (52) hide show
  1. package/agent-prompts/cursor_specific.md +13 -13
  2. package/bin/cccagent.mjs +17 -17
  3. package/config.sample.json +27 -27
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -277
  7. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  8. package/src/__tests__/builtin-config.test.ts +33 -33
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +224 -224
  11. package/src/__tests__/builtin-session-select.test.ts +116 -116
  12. package/src/__tests__/builtin-sigint.test.ts +56 -56
  13. package/src/__tests__/cards.test.ts +109 -109
  14. package/src/__tests__/ccc-adapter.test.ts +114 -114
  15. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  16. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  17. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  18. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  19. package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
  20. package/src/__tests__/config-reload.test.ts +10 -10
  21. package/src/__tests__/config-sample.test.ts +18 -18
  22. package/src/__tests__/cursor-adapter.test.ts +66 -7
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/jsonl-stream.test.ts +79 -79
  25. package/src/__tests__/orchestrator.test.ts +200 -200
  26. package/src/__tests__/raw-stream-log.test.ts +106 -106
  27. package/src/__tests__/session.test.ts +40 -40
  28. package/src/__tests__/sim-platform.test.ts +12 -12
  29. package/src/__tests__/web-ui.test.ts +209 -209
  30. package/src/adapters/ccc-adapter.ts +121 -121
  31. package/src/adapters/claude-adapter.ts +603 -603
  32. package/src/adapters/codex-adapter.ts +380 -380
  33. package/src/adapters/cursor-adapter.ts +39 -6
  34. package/src/adapters/jsonl-stream.ts +157 -157
  35. package/src/adapters/raw-stream-log.ts +124 -124
  36. package/src/agent-reload-config-rpc.ts +34 -34
  37. package/src/builtin/cli.ts +473 -473
  38. package/src/builtin/context.ts +323 -323
  39. package/src/builtin/file-tools.ts +1072 -1072
  40. package/src/builtin/index.ts +404 -404
  41. package/src/builtin/session-select.ts +48 -48
  42. package/src/builtin/sigint.ts +50 -50
  43. package/src/cards.ts +195 -195
  44. package/src/chatgpt-subscription-rpc.ts +27 -27
  45. package/src/chatgpt-subscription.ts +299 -299
  46. package/src/chrome-devtools-guard.ts +318 -318
  47. package/src/config.ts +125 -125
  48. package/src/feishu-api.ts +49 -49
  49. package/src/orchestrator.ts +179 -179
  50. package/src/runtime-reload.ts +34 -34
  51. package/src/session.ts +141 -141
  52. package/src/web-ui.ts +205 -205
@@ -1,116 +1,116 @@
1
- import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { describe, expect, it } from "vitest";
6
-
7
- import { defaultBuiltinSessionId } from "../builtin/context.ts";
8
- import { resolveBuiltinSession } from "../builtin/session-select.ts";
9
-
10
- async function writeSession(
11
- contextDir: string,
12
- sessionId: string,
13
- options: { cwd?: string; updatedAt: number; totalMessages?: number },
14
- ): Promise<void> {
15
- const dir = join(contextDir, sessionId);
16
- await mkdir(dir, { recursive: true });
17
- await writeFile(
18
- join(dir, "context.json"),
19
- JSON.stringify({
20
- version: 1,
21
- createdAt: options.updatedAt,
22
- updatedAt: options.updatedAt,
23
- sessionId,
24
- ...(options.cwd ? { cwd: options.cwd } : {}),
25
- summary: "",
26
- messages: [],
27
- totalMessages: options.totalMessages ?? 0,
28
- compactedMessages: 0,
29
- }),
30
- "utf8",
31
- );
32
- }
33
-
34
- describe("resolveBuiltinSession", () => {
35
- it("creates a fresh timestamp session by default", async () => {
36
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-session-select-new-"));
37
-
38
- const result = resolveBuiltinSession({
39
- cwd: "C:\\repo",
40
- contextDir,
41
- now: new Date(2026, 6, 2, 12, 15, 30),
42
- randomSuffix: "a1b2c3",
43
- });
44
-
45
- expect(result).toEqual({
46
- mode: "new",
47
- sessionId: "session-20260702-121530-a1b2c3",
48
- });
49
- });
50
-
51
- it("resumes an explicit existing session id", async () => {
52
- const contextDir = join(tmpdir(), `chatccc-session-select-explicit-${Date.now()}`);
53
- await writeSession(contextDir, "manual-session", { updatedAt: 1 });
54
-
55
- expect(resolveBuiltinSession({
56
- cwd: "C:\\repo",
57
- contextDir,
58
- resume: "manual-session",
59
- })).toEqual({
60
- mode: "resumed",
61
- sessionId: "manual-session",
62
- });
63
- });
64
-
65
- it("fails when an explicit session id does not exist", async () => {
66
- const contextDir = join(tmpdir(), `chatccc-session-select-missing-${Date.now()}`);
67
-
68
- expect(() => resolveBuiltinSession({
69
- cwd: "C:\\repo",
70
- contextDir,
71
- resume: "missing",
72
- })).toThrow("未找到 ccc 会话: missing");
73
- });
74
-
75
- it("resumes the newest session for cwd when resume has no id", async () => {
76
- const contextDir = join(tmpdir(), `chatccc-session-select-cwd-${Date.now()}`);
77
- await writeSession(contextDir, "old-match", { cwd: "C:\\repo", updatedAt: 1_000 });
78
- await writeSession(contextDir, "new-match", { cwd: "C:\\repo", updatedAt: 2_000 });
79
- await writeSession(contextDir, "other-cwd", { cwd: "C:\\other", updatedAt: 3_000 });
80
-
81
- expect(resolveBuiltinSession({
82
- cwd: "C:\\repo",
83
- contextDir,
84
- resume: true,
85
- })).toEqual({
86
- mode: "resumed",
87
- sessionId: "new-match",
88
- });
89
- });
90
-
91
- it("resumes legacy cwd-hash sessions when resume has no id", async () => {
92
- const contextDir = join(tmpdir(), `chatccc-session-select-legacy-${Date.now()}`);
93
- const cwd = "C:\\repo";
94
- const legacySessionId = defaultBuiltinSessionId(cwd);
95
- await writeSession(contextDir, legacySessionId, { updatedAt: 1_000 });
96
-
97
- expect(resolveBuiltinSession({
98
- cwd,
99
- contextDir,
100
- resume: true,
101
- })).toEqual({
102
- mode: "resumed",
103
- sessionId: legacySessionId,
104
- });
105
- });
106
-
107
- it("fails when there is no cwd session to resume", async () => {
108
- const contextDir = join(tmpdir(), `chatccc-session-select-empty-${Date.now()}`);
109
-
110
- expect(() => resolveBuiltinSession({
111
- cwd: "C:\\repo",
112
- contextDir,
113
- resume: true,
114
- })).toThrow("未找到当前目录可恢复的 ccc 会话");
115
- });
116
- });
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import { defaultBuiltinSessionId } from "../builtin/context.ts";
8
+ import { resolveBuiltinSession } from "../builtin/session-select.ts";
9
+
10
+ async function writeSession(
11
+ contextDir: string,
12
+ sessionId: string,
13
+ options: { cwd?: string; updatedAt: number; totalMessages?: number },
14
+ ): Promise<void> {
15
+ const dir = join(contextDir, sessionId);
16
+ await mkdir(dir, { recursive: true });
17
+ await writeFile(
18
+ join(dir, "context.json"),
19
+ JSON.stringify({
20
+ version: 1,
21
+ createdAt: options.updatedAt,
22
+ updatedAt: options.updatedAt,
23
+ sessionId,
24
+ ...(options.cwd ? { cwd: options.cwd } : {}),
25
+ summary: "",
26
+ messages: [],
27
+ totalMessages: options.totalMessages ?? 0,
28
+ compactedMessages: 0,
29
+ }),
30
+ "utf8",
31
+ );
32
+ }
33
+
34
+ describe("resolveBuiltinSession", () => {
35
+ it("creates a fresh timestamp session by default", async () => {
36
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-session-select-new-"));
37
+
38
+ const result = resolveBuiltinSession({
39
+ cwd: "C:\\repo",
40
+ contextDir,
41
+ now: new Date(2026, 6, 2, 12, 15, 30),
42
+ randomSuffix: "a1b2c3",
43
+ });
44
+
45
+ expect(result).toEqual({
46
+ mode: "new",
47
+ sessionId: "session-20260702-121530-a1b2c3",
48
+ });
49
+ });
50
+
51
+ it("resumes an explicit existing session id", async () => {
52
+ const contextDir = join(tmpdir(), `chatccc-session-select-explicit-${Date.now()}`);
53
+ await writeSession(contextDir, "manual-session", { updatedAt: 1 });
54
+
55
+ expect(resolveBuiltinSession({
56
+ cwd: "C:\\repo",
57
+ contextDir,
58
+ resume: "manual-session",
59
+ })).toEqual({
60
+ mode: "resumed",
61
+ sessionId: "manual-session",
62
+ });
63
+ });
64
+
65
+ it("fails when an explicit session id does not exist", async () => {
66
+ const contextDir = join(tmpdir(), `chatccc-session-select-missing-${Date.now()}`);
67
+
68
+ expect(() => resolveBuiltinSession({
69
+ cwd: "C:\\repo",
70
+ contextDir,
71
+ resume: "missing",
72
+ })).toThrow("未找到 ccc 会话: missing");
73
+ });
74
+
75
+ it("resumes the newest session for cwd when resume has no id", async () => {
76
+ const contextDir = join(tmpdir(), `chatccc-session-select-cwd-${Date.now()}`);
77
+ await writeSession(contextDir, "old-match", { cwd: "C:\\repo", updatedAt: 1_000 });
78
+ await writeSession(contextDir, "new-match", { cwd: "C:\\repo", updatedAt: 2_000 });
79
+ await writeSession(contextDir, "other-cwd", { cwd: "C:\\other", updatedAt: 3_000 });
80
+
81
+ expect(resolveBuiltinSession({
82
+ cwd: "C:\\repo",
83
+ contextDir,
84
+ resume: true,
85
+ })).toEqual({
86
+ mode: "resumed",
87
+ sessionId: "new-match",
88
+ });
89
+ });
90
+
91
+ it("resumes legacy cwd-hash sessions when resume has no id", async () => {
92
+ const contextDir = join(tmpdir(), `chatccc-session-select-legacy-${Date.now()}`);
93
+ const cwd = "C:\\repo";
94
+ const legacySessionId = defaultBuiltinSessionId(cwd);
95
+ await writeSession(contextDir, legacySessionId, { updatedAt: 1_000 });
96
+
97
+ expect(resolveBuiltinSession({
98
+ cwd,
99
+ contextDir,
100
+ resume: true,
101
+ })).toEqual({
102
+ mode: "resumed",
103
+ sessionId: legacySessionId,
104
+ });
105
+ });
106
+
107
+ it("fails when there is no cwd session to resume", async () => {
108
+ const contextDir = join(tmpdir(), `chatccc-session-select-empty-${Date.now()}`);
109
+
110
+ expect(() => resolveBuiltinSession({
111
+ cwd: "C:\\repo",
112
+ contextDir,
113
+ resume: true,
114
+ })).toThrow("未找到当前目录可恢复的 ccc 会话");
115
+ });
116
+ });
@@ -1,56 +1,56 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { createCtrlCState } from "../builtin/sigint.ts";
4
-
5
- describe("builtin CLI Ctrl+C state", () => {
6
- it("requires two presses to interrupt an active generation", () => {
7
- let now = 1_000;
8
- const state = createCtrlCState({ windowMs: 2_000, now: () => now });
9
-
10
- expect(state.press(true)).toBe("arm-interrupt");
11
- expect(state.press(true)).toBe("interrupt");
12
- });
13
-
14
- it("requires two presses to exit while idle", () => {
15
- let now = 1_000;
16
- const state = createCtrlCState({ windowMs: 2_000, now: () => now });
17
-
18
- expect(state.press(false)).toBe("arm-exit");
19
- expect(state.press(false)).toBe("exit");
20
- });
21
-
22
- it("expires the pending confirmation after the window", () => {
23
- let now = 1_000;
24
- const state = createCtrlCState({ windowMs: 2_000, now: () => now });
25
-
26
- expect(state.press(true)).toBe("arm-interrupt");
27
-
28
- now += 2_001;
29
-
30
- expect(state.press(true)).toBe("arm-interrupt");
31
- expect(state.press(true)).toBe("interrupt");
32
- });
33
-
34
- it("does not convert a pending interrupt into an idle exit after reset", () => {
35
- let now = 1_000;
36
- const state = createCtrlCState({ windowMs: 2_000, now: () => now });
37
-
38
- expect(state.press(true)).toBe("arm-interrupt");
39
-
40
- state.reset();
41
- now += 1_000;
42
-
43
- expect(state.press(false)).toBe("arm-exit");
44
- });
45
-
46
- it("does not confirm a different action with the second press", () => {
47
- let now = 1_000;
48
- const state = createCtrlCState({ windowMs: 2_000, now: () => now });
49
-
50
- expect(state.press(true)).toBe("arm-interrupt");
51
- now += 500;
52
-
53
- expect(state.press(false)).toBe("arm-exit");
54
- expect(state.press(false)).toBe("exit");
55
- });
56
- });
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createCtrlCState } from "../builtin/sigint.ts";
4
+
5
+ describe("builtin CLI Ctrl+C state", () => {
6
+ it("requires two presses to interrupt an active generation", () => {
7
+ let now = 1_000;
8
+ const state = createCtrlCState({ windowMs: 2_000, now: () => now });
9
+
10
+ expect(state.press(true)).toBe("arm-interrupt");
11
+ expect(state.press(true)).toBe("interrupt");
12
+ });
13
+
14
+ it("requires two presses to exit while idle", () => {
15
+ let now = 1_000;
16
+ const state = createCtrlCState({ windowMs: 2_000, now: () => now });
17
+
18
+ expect(state.press(false)).toBe("arm-exit");
19
+ expect(state.press(false)).toBe("exit");
20
+ });
21
+
22
+ it("expires the pending confirmation after the window", () => {
23
+ let now = 1_000;
24
+ const state = createCtrlCState({ windowMs: 2_000, now: () => now });
25
+
26
+ expect(state.press(true)).toBe("arm-interrupt");
27
+
28
+ now += 2_001;
29
+
30
+ expect(state.press(true)).toBe("arm-interrupt");
31
+ expect(state.press(true)).toBe("interrupt");
32
+ });
33
+
34
+ it("does not convert a pending interrupt into an idle exit after reset", () => {
35
+ let now = 1_000;
36
+ const state = createCtrlCState({ windowMs: 2_000, now: () => now });
37
+
38
+ expect(state.press(true)).toBe("arm-interrupt");
39
+
40
+ state.reset();
41
+ now += 1_000;
42
+
43
+ expect(state.press(false)).toBe("arm-exit");
44
+ });
45
+
46
+ it("does not confirm a different action with the second press", () => {
47
+ let now = 1_000;
48
+ const state = createCtrlCState({ windowMs: 2_000, now: () => now });
49
+
50
+ expect(state.press(true)).toBe("arm-interrupt");
51
+ now += 500;
52
+
53
+ expect(state.press(false)).toBe("arm-exit");
54
+ expect(state.press(false)).toBe("exit");
55
+ });
56
+ });
@@ -1,18 +1,18 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import {
3
- buildProgressCard,
4
- buildHelpCard,
2
+ import {
3
+ buildProgressCard,
4
+ buildHelpCard,
5
5
  buildCdContent,
6
6
  buildCdCard,
7
7
  buildSessionsCard,
8
- buildStatusCard,
9
- buildCodexUsageCard,
10
- buildCodexResetConfirmCard,
11
- buildButtons,
12
- truncateContent,
13
- getToolEmoji,
14
- } from "../cards.ts";
15
- import { ABD_HELP_LINE } from "../shared-prefix.ts";
8
+ buildStatusCard,
9
+ buildCodexUsageCard,
10
+ buildCodexResetConfirmCard,
11
+ buildButtons,
12
+ truncateContent,
13
+ getToolEmoji,
14
+ } from "../cards.ts";
15
+ import { ABD_HELP_LINE } from "../shared-prefix.ts";
16
16
 
17
17
  // ---------------------------------------------------------------------------
18
18
  // truncateContent
@@ -154,32 +154,32 @@ describe("buildHelpCard", () => {
154
154
  expect(parsed.elements[0].text.content).toContain("你好");
155
155
  });
156
156
 
157
- it("includes action buttons", () => {
158
- const card = buildHelpCard("test");
159
- const parsed = JSON.parse(card);
160
- const action = parsed.elements[2];
161
- expect(action.tag).toBe("action");
162
- expect(action.actions).toHaveLength(7);
163
- });
164
-
165
- it("adds ABD prefix help as the final help line", () => {
166
- const card = buildHelpCard("test");
167
- const parsed = JSON.parse(card);
168
- const lines = parsed.elements[1].text.content.split("\n");
169
-
170
- expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
171
- expect(lines.at(-1)).toBe(ABD_HELP_LINE);
172
- });
173
-
174
- it("does not advertise the hidden ccc agent", () => {
175
- const card = buildHelpCard("test");
176
- const parsed = JSON.parse(card);
177
- const text = JSON.stringify(parsed);
178
-
179
- expect(text).not.toContain("/new ccc");
180
- expect(text).not.toContain("CCC Agent");
181
- });
182
- });
157
+ it("includes action buttons", () => {
158
+ const card = buildHelpCard("test");
159
+ const parsed = JSON.parse(card);
160
+ const action = parsed.elements[2];
161
+ expect(action.tag).toBe("action");
162
+ expect(action.actions).toHaveLength(7);
163
+ });
164
+
165
+ it("adds ABD prefix help as the final help line", () => {
166
+ const card = buildHelpCard("test");
167
+ const parsed = JSON.parse(card);
168
+ const lines = parsed.elements[1].text.content.split("\n");
169
+
170
+ expect(lines).toContain("发送 **/usage** 查看 Codex 5h/周用量,以及查询/使用主动重置卡");
171
+ expect(lines.at(-1)).toBe(ABD_HELP_LINE);
172
+ });
173
+
174
+ it("does not advertise the hidden ccc agent", () => {
175
+ const card = buildHelpCard("test");
176
+ const parsed = JSON.parse(card);
177
+ const text = JSON.stringify(parsed);
178
+
179
+ expect(text).not.toContain("/new ccc");
180
+ expect(text).not.toContain("CCC Agent");
181
+ });
182
+ });
183
183
 
184
184
  // ---------------------------------------------------------------------------
185
185
  // buildCdContent
@@ -373,29 +373,29 @@ describe("buildSessionsCard", () => {
373
373
  expect(content).toContain("Cursor 会话");
374
374
  });
375
375
 
376
- it("omits Cursor section when no Cursor sessions", () => {
377
- const card = buildSessionsCard([
378
- { sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
379
- ]);
380
- const parsed = JSON.parse(card);
381
- const content: string = parsed.elements[0].text.content;
382
- expect(content).toContain("Claude Code 会话");
383
- expect(content).not.toContain("Cursor 会话");
384
- });
385
-
386
- it("labels hidden ccc sessions without grouping them as Claude Code", () => {
387
- const card = buildSessionsCard([
388
- { sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
389
- ]);
390
- const parsed = JSON.parse(card);
391
- const content: string = parsed.elements[0].text.content;
392
-
393
- expect(content).toContain("CCC Agent 会话");
394
- expect(content).toContain("工具: CCC Agent");
395
- expect(content).not.toContain("Claude Code 会话");
396
- });
397
-
398
- it("displays chatName when provided", () => {
376
+ it("omits Cursor section when no Cursor sessions", () => {
377
+ const card = buildSessionsCard([
378
+ { sessionId: "c1", chatName: "", chatId: "oc_c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
379
+ ]);
380
+ const parsed = JSON.parse(card);
381
+ const content: string = parsed.elements[0].text.content;
382
+ expect(content).toContain("Claude Code 会话");
383
+ expect(content).not.toContain("Cursor 会话");
384
+ });
385
+
386
+ it("labels hidden ccc sessions without grouping them as Claude Code", () => {
387
+ const card = buildSessionsCard([
388
+ { sessionId: "session-20260702-121530-a1b2c3", chatName: "", chatId: "oc_ccc", active: false, turnCount: 1, elapsedSeconds: null, model: "deepseek-v4-pro", tool: "ccc" },
389
+ ]);
390
+ const parsed = JSON.parse(card);
391
+ const content: string = parsed.elements[0].text.content;
392
+
393
+ expect(content).toContain("CCC Agent 会话");
394
+ expect(content).toContain("工具: CCC Agent");
395
+ expect(content).not.toContain("Claude Code 会话");
396
+ });
397
+
398
+ it("displays chatName when provided", () => {
399
399
  const card = buildSessionsCard([
400
400
  { sessionId: "abc123", chatName: "帮我写代码-src", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
401
401
  ]);
@@ -446,7 +446,7 @@ describe("buildSessionsCard", () => {
446
446
  // buildStatusCard
447
447
  // ---------------------------------------------------------------------------
448
448
 
449
- describe("buildStatusCard", () => {
449
+ describe("buildStatusCard", () => {
450
450
  it("returns valid JSON with status text", () => {
451
451
  const card = buildStatusCard("一切正常");
452
452
  const parsed = JSON.parse(card);
@@ -466,54 +466,54 @@ describe("buildStatusCard", () => {
466
466
  const action = parsed.elements[2];
467
467
  expect(action.actions[0].text.content).toBe("收起");
468
468
  });
469
- });
470
-
471
- // ---------------------------------------------------------------------------
472
- // buildCodexUsageCard / buildCodexResetConfirmCard
473
- // ---------------------------------------------------------------------------
474
-
475
- describe("Codex usage reset cards", () => {
476
- it("shows the reset button only when reset credits are available", () => {
477
- const card = buildCodexUsageCard("Codex 用量", 2);
478
- const parsed = JSON.parse(card);
479
- const action = parsed.elements.find((element: any) => element.tag === "action");
480
- expect(action.actions[0].text.content).toBe("发起重置");
481
- expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
482
-
483
- const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
484
- const noCreditParsed = JSON.parse(noCreditCard);
485
- expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
486
- });
487
-
488
- it("builds yes/no confirmation buttons tied to the parent usage card", () => {
489
- const card = buildCodexResetConfirmCard({
490
- availableCount: 2,
491
- parentMessageId: "usage-message",
492
- requestId: "request-1",
493
- });
494
- const parsed = JSON.parse(card);
495
- const action = parsed.elements.find((element: any) => element.tag === "action");
496
-
497
- expect(action.actions[0].text.content).toBe("是,发起重置");
498
- expect(action.actions[0].value).toEqual({
499
- action: "codex_reset_confirm",
500
- decision: "yes",
501
- parentMessageId: "usage-message",
502
- requestId: "request-1",
503
- });
504
- expect(action.actions[1].text.content).toBe("否");
505
- expect(action.actions[1].value).toEqual({
506
- action: "codex_reset_confirm",
507
- decision: "no",
508
- parentMessageId: "usage-message",
509
- requestId: "request-1",
510
- });
511
- });
512
- });
513
-
514
- // ---------------------------------------------------------------------------
515
- // buildButtons
516
- // ---------------------------------------------------------------------------
469
+ });
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // buildCodexUsageCard / buildCodexResetConfirmCard
473
+ // ---------------------------------------------------------------------------
474
+
475
+ describe("Codex usage reset cards", () => {
476
+ it("shows the reset button only when reset credits are available", () => {
477
+ const card = buildCodexUsageCard("Codex 用量", 2);
478
+ const parsed = JSON.parse(card);
479
+ const action = parsed.elements.find((element: any) => element.tag === "action");
480
+ expect(action.actions[0].text.content).toBe("发起重置");
481
+ expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
482
+
483
+ const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
484
+ const noCreditParsed = JSON.parse(noCreditCard);
485
+ expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
486
+ });
487
+
488
+ it("builds yes/no confirmation buttons tied to the parent usage card", () => {
489
+ const card = buildCodexResetConfirmCard({
490
+ availableCount: 2,
491
+ parentMessageId: "usage-message",
492
+ requestId: "request-1",
493
+ });
494
+ const parsed = JSON.parse(card);
495
+ const action = parsed.elements.find((element: any) => element.tag === "action");
496
+
497
+ expect(action.actions[0].text.content).toBe("是,发起重置");
498
+ expect(action.actions[0].value).toEqual({
499
+ action: "codex_reset_confirm",
500
+ decision: "yes",
501
+ parentMessageId: "usage-message",
502
+ requestId: "request-1",
503
+ });
504
+ expect(action.actions[1].text.content).toBe("否");
505
+ expect(action.actions[1].value).toEqual({
506
+ action: "codex_reset_confirm",
507
+ decision: "no",
508
+ parentMessageId: "usage-message",
509
+ requestId: "request-1",
510
+ });
511
+ });
512
+ });
513
+
514
+ // ---------------------------------------------------------------------------
515
+ // buildButtons
516
+ // ---------------------------------------------------------------------------
517
517
 
518
518
  describe("buildButtons", () => {
519
519
  it("returns action with buttons", () => {