chatccc 0.2.199 → 0.2.200
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.
- package/README.md +1 -1
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +277 -277
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -224
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/card-plain-text.test.ts +5 -5
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -114
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +87 -45
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/orchestrator.test.ts +227 -202
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +380 -380
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +81 -81
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +473 -473
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -1072
- package/src/builtin/index.ts +404 -404
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/config.ts +125 -125
- package/src/feishu-api.ts +86 -63
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +183 -183
- package/src/privacy.ts +118 -118
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +6 -6
- package/src/session-name.ts +8 -8
- package/src/session.ts +141 -141
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/turn-cards.ts +117 -117
- package/src/web-ui.ts +205 -205
|
@@ -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/7天用量窗口,以及查询/使用主动重置卡");
|
|
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,114 +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
|
-
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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
|
|
5
|
-
|
|
6
|
-
vi.mock("../chatgpt-subscription.ts", () => ({
|
|
7
|
-
getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
CHATGPT_SUBSCRIPTION_PATH,
|
|
12
|
-
handleChatGptSubscriptionRequest,
|
|
13
|
-
} from "../chatgpt-subscription-rpc.ts";
|
|
14
|
-
|
|
15
|
-
function request(path = CHATGPT_SUBSCRIPTION_PATH, method = "POST"): Readable & {
|
|
16
|
-
url?: string;
|
|
17
|
-
method?: string;
|
|
18
|
-
headers: Record<string, string>;
|
|
19
|
-
} {
|
|
20
|
-
const req = Readable.from([]) as Readable & {
|
|
21
|
-
url?: string;
|
|
22
|
-
method?: string;
|
|
23
|
-
headers: Record<string, string>;
|
|
24
|
-
};
|
|
25
|
-
req.url = path;
|
|
26
|
-
req.method = method;
|
|
27
|
-
req.headers = {};
|
|
28
|
-
return req;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function response() {
|
|
32
|
-
const res = {
|
|
33
|
-
statusCode: 0,
|
|
34
|
-
headers: {} as Record<string, string>,
|
|
35
|
-
body: "",
|
|
36
|
-
writeHead(status: number, headers: Record<string, string>) {
|
|
37
|
-
this.statusCode = status;
|
|
38
|
-
this.headers = headers;
|
|
39
|
-
return this;
|
|
40
|
-
},
|
|
41
|
-
end(chunk?: string) {
|
|
42
|
-
this.body += chunk ?? "";
|
|
43
|
-
return this;
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
return res;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe("ChatGPT subscription RPC", () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
mockGetChatGptSubscriptionStatus.mockReset();
|
|
52
|
-
mockGetChatGptSubscriptionStatus.mockResolvedValue({
|
|
53
|
-
ok: false,
|
|
54
|
-
code: "chrome_cdp_disabled",
|
|
55
|
-
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
56
|
-
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("returns the structured subscription lookup JSON", async () => {
|
|
61
|
-
const req = request();
|
|
62
|
-
const res = response();
|
|
63
|
-
|
|
64
|
-
await expect(handleChatGptSubscriptionRequest(req as never, res as never)).resolves.toBe(true);
|
|
65
|
-
|
|
66
|
-
expect(res.statusCode).toBe(200);
|
|
67
|
-
expect(JSON.parse(res.body)).toEqual({
|
|
68
|
-
ok: false,
|
|
69
|
-
code: "chrome_cdp_disabled",
|
|
70
|
-
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
71
|
-
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("does not handle other paths", async () => {
|
|
76
|
-
const res = response();
|
|
77
|
-
|
|
78
|
-
await expect(handleChatGptSubscriptionRequest(request("/api/other") as never, res as never)).resolves.toBe(false);
|
|
79
|
-
expect(res.body).toBe("");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("rejects non-POST methods", async () => {
|
|
83
|
-
const res = response();
|
|
84
|
-
|
|
85
|
-
await expect(handleChatGptSubscriptionRequest(request(CHATGPT_SUBSCRIPTION_PATH, "GET") as never, res as never)).resolves.toBe(true);
|
|
86
|
-
expect(res.statusCode).toBe(405);
|
|
87
|
-
expect(JSON.parse(res.body)).toMatchObject({ ok: false, code: "method_not_allowed" });
|
|
88
|
-
});
|
|
89
|
-
});
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mockGetChatGptSubscriptionStatus = vi.hoisted(() => vi.fn());
|
|
5
|
+
|
|
6
|
+
vi.mock("../chatgpt-subscription.ts", () => ({
|
|
7
|
+
getChatGptSubscriptionStatus: mockGetChatGptSubscriptionStatus,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CHATGPT_SUBSCRIPTION_PATH,
|
|
12
|
+
handleChatGptSubscriptionRequest,
|
|
13
|
+
} from "../chatgpt-subscription-rpc.ts";
|
|
14
|
+
|
|
15
|
+
function request(path = CHATGPT_SUBSCRIPTION_PATH, method = "POST"): Readable & {
|
|
16
|
+
url?: string;
|
|
17
|
+
method?: string;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
} {
|
|
20
|
+
const req = Readable.from([]) as Readable & {
|
|
21
|
+
url?: string;
|
|
22
|
+
method?: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
req.url = path;
|
|
26
|
+
req.method = method;
|
|
27
|
+
req.headers = {};
|
|
28
|
+
return req;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function response() {
|
|
32
|
+
const res = {
|
|
33
|
+
statusCode: 0,
|
|
34
|
+
headers: {} as Record<string, string>,
|
|
35
|
+
body: "",
|
|
36
|
+
writeHead(status: number, headers: Record<string, string>) {
|
|
37
|
+
this.statusCode = status;
|
|
38
|
+
this.headers = headers;
|
|
39
|
+
return this;
|
|
40
|
+
},
|
|
41
|
+
end(chunk?: string) {
|
|
42
|
+
this.body += chunk ?? "";
|
|
43
|
+
return this;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("ChatGPT subscription RPC", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mockGetChatGptSubscriptionStatus.mockReset();
|
|
52
|
+
mockGetChatGptSubscriptionStatus.mockResolvedValue({
|
|
53
|
+
ok: false,
|
|
54
|
+
code: "chrome_cdp_disabled",
|
|
55
|
+
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
56
|
+
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns the structured subscription lookup JSON", async () => {
|
|
61
|
+
const req = request();
|
|
62
|
+
const res = response();
|
|
63
|
+
|
|
64
|
+
await expect(handleChatGptSubscriptionRequest(req as never, res as never)).resolves.toBe(true);
|
|
65
|
+
|
|
66
|
+
expect(res.statusCode).toBe(200);
|
|
67
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
68
|
+
ok: false,
|
|
69
|
+
code: "chrome_cdp_disabled",
|
|
70
|
+
reason: "Chrome CDP guard is disabled in ChatCCC config.",
|
|
71
|
+
chromeCdp: { enabled: false, port: 15166, status: "skipped" },
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("does not handle other paths", async () => {
|
|
76
|
+
const res = response();
|
|
77
|
+
|
|
78
|
+
await expect(handleChatGptSubscriptionRequest(request("/api/other") as never, res as never)).resolves.toBe(false);
|
|
79
|
+
expect(res.body).toBe("");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("rejects non-POST methods", async () => {
|
|
83
|
+
const res = response();
|
|
84
|
+
|
|
85
|
+
await expect(handleChatGptSubscriptionRequest(request(CHATGPT_SUBSCRIPTION_PATH, "GET") as never, res as never)).resolves.toBe(true);
|
|
86
|
+
expect(res.statusCode).toBe(405);
|
|
87
|
+
expect(JSON.parse(res.body)).toMatchObject({ ok: false, code: "method_not_allowed" });
|
|
88
|
+
});
|
|
89
|
+
});
|