chatccc 0.2.195 → 0.2.197

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 (51) 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 -0
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -181
  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 -196
  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 -113
  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 -0
  19. package/src/__tests__/codex-raw-stream-log.test.ts +120 -0
  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 +99 -2
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/orchestrator.test.ts +181 -154
  25. package/src/__tests__/raw-stream-log.test.ts +106 -106
  26. package/src/__tests__/session.test.ts +40 -40
  27. package/src/__tests__/sim-platform.test.ts +12 -12
  28. package/src/__tests__/web-ui.test.ts +209 -209
  29. package/src/adapters/ccc-adapter.ts +121 -119
  30. package/src/adapters/claude-adapter.ts +603 -566
  31. package/src/adapters/codex-adapter.ts +57 -32
  32. package/src/adapters/cursor-adapter.ts +182 -57
  33. package/src/adapters/raw-stream-log.ts +124 -124
  34. package/src/agent-reload-config-rpc.ts +34 -0
  35. package/src/builtin/cli.ts +473 -461
  36. package/src/builtin/context.ts +323 -323
  37. package/src/builtin/file-tools.ts +1072 -915
  38. package/src/builtin/index.ts +404 -353
  39. package/src/builtin/session-select.ts +48 -48
  40. package/src/builtin/sigint.ts +50 -50
  41. package/src/cards.ts +195 -195
  42. package/src/chatgpt-subscription-rpc.ts +27 -27
  43. package/src/chatgpt-subscription.ts +299 -299
  44. package/src/chrome-devtools-guard.ts +318 -318
  45. package/src/config.ts +125 -125
  46. package/src/feishu-api.ts +49 -49
  47. package/src/index.ts +8 -13
  48. package/src/orchestrator.ts +166 -145
  49. package/src/runtime-reload.ts +34 -0
  50. package/src/session.ts +132 -130
  51. package/src/web-ui.ts +205 -205
@@ -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", () => {
@@ -1,113 +1,114 @@
1
- import { mkdtemp } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- import { afterEach, describe, expect, it, vi } from "vitest";
6
-
7
- const streamTextMock = vi.fn();
8
- const generateTextMock = vi.fn();
9
-
10
- vi.mock("@ai-sdk/openai-compatible", () => ({
11
- createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
12
- }));
13
-
14
- vi.mock("ai", () => ({
15
- streamText: streamTextMock,
16
- generateText: generateTextMock,
17
- stepCountIs: vi.fn((count: number) => ({ count })),
18
- jsonSchema: vi.fn((schema: unknown) => schema),
19
- tool: vi.fn((definition: unknown) => definition),
20
- }));
21
-
22
- async function* textStream(...chunks: string[]): AsyncIterable<string> {
23
- for (const chunk of chunks) yield chunk;
24
- }
25
-
26
- async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
27
- for (const part of parts) yield part;
28
- }
29
-
30
- afterEach(() => {
31
- streamTextMock.mockReset();
32
- generateTextMock.mockReset();
33
- });
34
-
35
- describe("createCccAdapter", () => {
36
- it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
37
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
38
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
39
- const adapter = createCccAdapter({
40
- apiKey: "sk-test",
41
- contextDir,
42
- model: "deepseek-v4-pro",
43
- });
44
-
45
- const created = await adapter.createSession("F:\\repo");
46
- const info = await adapter.getSessionInfo(created.sessionId);
47
-
48
- expect(created.sessionId).toMatch(/^session-\d{8}-\d{6}-[a-f0-9]{6}$/);
49
- expect(info).toEqual(expect.objectContaining({
50
- sessionId: created.sessionId,
51
- cwd: "F:\\repo",
52
- model: "deepseek-v4-pro",
53
- }));
54
- });
55
-
56
- it("maps ChatSession text chunks to unified assistant text blocks", async () => {
57
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
58
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
59
- const adapter = createCccAdapter({
60
- apiKey: "sk-test",
61
- contextDir,
62
- model: "deepseek-v4-flash",
63
- });
64
- const { sessionId } = await adapter.createSession("F:\\repo");
65
- streamTextMock.mockReturnValueOnce({ textStream: textStream("hello", " world") });
66
-
67
- const messages = [];
68
- for await (const message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
69
- messages.push(message);
70
- }
71
-
72
- expect(messages).toEqual([
73
- { type: "assistant", blocks: [{ type: "text", text: "hello" }] },
74
- { type: "assistant", blocks: [{ type: "text", text: " world" }] },
75
- ]);
76
- expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
77
- model: { modelId: "deepseek-v4-flash" },
78
- }));
79
- });
80
-
81
- it("maps ChatSession tool events to unified tool blocks", async () => {
82
- const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
83
- const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
84
- const adapter = createCccAdapter({
85
- apiKey: "sk-test",
86
- contextDir,
87
- model: "deepseek-v4-flash",
88
- });
89
- const { sessionId } = await adapter.createSession("F:\\repo");
90
- streamTextMock.mockReturnValueOnce({
91
- fullStream: fullStream(
92
- { type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
93
- { type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
94
- ),
95
- });
96
-
97
- const messages = [];
98
- for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
99
- messages.push(message);
100
- }
101
-
102
- expect(messages).toEqual([
103
- {
104
- type: "assistant",
105
- blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
106
- },
107
- {
108
- type: "assistant",
109
- blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
110
- },
111
- ]);
112
- });
113
- });
1
+ import { mkdtemp } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ const streamTextMock = vi.fn();
8
+ const generateTextMock = vi.fn();
9
+
10
+ vi.mock("@ai-sdk/openai-compatible", () => ({
11
+ createOpenAICompatible: vi.fn(() => (modelId: string) => ({ modelId })),
12
+ }));
13
+
14
+ vi.mock("ai", () => ({
15
+ streamText: streamTextMock,
16
+ generateText: generateTextMock,
17
+ isLoopFinished: vi.fn(() => ({ loopFinished: true })),
18
+ stepCountIs: vi.fn((count: number) => ({ count })),
19
+ jsonSchema: vi.fn((schema: unknown) => schema),
20
+ tool: vi.fn((definition: unknown) => definition),
21
+ }));
22
+
23
+ async function* textStream(...chunks: string[]): AsyncIterable<string> {
24
+ for (const chunk of chunks) yield chunk;
25
+ }
26
+
27
+ async function* fullStream(...parts: unknown[]): AsyncIterable<unknown> {
28
+ for (const part of parts) yield part;
29
+ }
30
+
31
+ afterEach(() => {
32
+ streamTextMock.mockReset();
33
+ generateTextMock.mockReset();
34
+ });
35
+
36
+ describe("createCccAdapter", () => {
37
+ it("creates a persisted ccc session and exposes model/cwd metadata", async () => {
38
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
39
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-meta-"));
40
+ const adapter = createCccAdapter({
41
+ apiKey: "sk-test",
42
+ contextDir,
43
+ model: "deepseek-v4-pro",
44
+ });
45
+
46
+ const created = await adapter.createSession("F:\\repo");
47
+ const info = await adapter.getSessionInfo(created.sessionId);
48
+
49
+ expect(created.sessionId).toMatch(/^session-\d{8}-\d{6}-[a-f0-9]{6}$/);
50
+ expect(info).toEqual(expect.objectContaining({
51
+ sessionId: created.sessionId,
52
+ cwd: "F:\\repo",
53
+ model: "deepseek-v4-pro",
54
+ }));
55
+ });
56
+
57
+ it("maps ChatSession text chunks to unified assistant text blocks", async () => {
58
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
59
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-stream-"));
60
+ const adapter = createCccAdapter({
61
+ apiKey: "sk-test",
62
+ contextDir,
63
+ model: "deepseek-v4-flash",
64
+ });
65
+ const { sessionId } = await adapter.createSession("F:\\repo");
66
+ streamTextMock.mockReturnValueOnce({ textStream: textStream("hello", " world") });
67
+
68
+ const messages = [];
69
+ for await (const message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
70
+ messages.push(message);
71
+ }
72
+
73
+ expect(messages).toEqual([
74
+ { type: "assistant", blocks: [{ type: "text", text: "hello" }] },
75
+ { type: "assistant", blocks: [{ type: "text", text: " world" }] },
76
+ ]);
77
+ expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
78
+ model: { modelId: "deepseek-v4-flash" },
79
+ }));
80
+ });
81
+
82
+ it("maps ChatSession tool events to unified tool blocks", async () => {
83
+ const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
84
+ const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
85
+ const adapter = createCccAdapter({
86
+ apiKey: "sk-test",
87
+ contextDir,
88
+ model: "deepseek-v4-flash",
89
+ });
90
+ const { sessionId } = await adapter.createSession("F:\\repo");
91
+ streamTextMock.mockReturnValueOnce({
92
+ fullStream: fullStream(
93
+ { type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
94
+ { type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
95
+ ),
96
+ });
97
+
98
+ const messages = [];
99
+ for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
100
+ messages.push(message);
101
+ }
102
+
103
+ expect(messages).toEqual([
104
+ {
105
+ type: "assistant",
106
+ blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
107
+ },
108
+ {
109
+ type: "assistant",
110
+ blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
111
+ },
112
+ ]);
113
+ });
114
+ });