chatccc 0.2.201 → 0.2.202
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 +17 -17
- 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 +133 -122
- 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__/cursor-adapter.test.ts +226 -226
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +90 -90
- 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 +464 -301
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +46 -44
- 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/cursor-adapter.ts +673 -673
- 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 +222 -210
- 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 +118 -118
- 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 +348 -329
- 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 +177 -169
- 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/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
|
-
});
|
|
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
|
|
@@ -258,7 +258,7 @@ describe("buildCdCard", () => {
|
|
|
258
258
|
it("shows current working directory in markdown", () => {
|
|
259
259
|
const card = buildCdCard("/home/project", entries, []);
|
|
260
260
|
const parsed = JSON.parse(card);
|
|
261
|
-
const cwdContent = mdContents(parsed).find((c) => c.includes("
|
|
261
|
+
const cwdContent = mdContents(parsed).find((c) => c.includes("后续新建会话默认工作路径"));
|
|
262
262
|
expect(cwdContent).toBeDefined();
|
|
263
263
|
expect(cwdContent).toContain("/home/project");
|
|
264
264
|
});
|
|
@@ -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
|
]);
|
|
@@ -403,19 +403,20 @@ describe("buildSessionsCard", () => {
|
|
|
403
403
|
expect(parsed.elements[0].text.content).toContain("帮我写代码-src");
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
it("shows (群聊) tag for group chat sessions and not for private chats", () => {
|
|
407
|
-
const card = buildSessionsCard([
|
|
408
|
-
{ sessionId: "g1", chatName: "group-chat", chatId: "oc_group1", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
409
|
-
{ sessionId: "p1", chatName: "private-chat", chatId: "
|
|
410
|
-
]);
|
|
411
|
-
const content: string = JSON.parse(card).elements[0].text.content;
|
|
412
|
-
expect(content).toContain("(群聊)");
|
|
406
|
+
it("shows (群聊) tag for group chat sessions and not for private chats", () => {
|
|
407
|
+
const card = buildSessionsCard([
|
|
408
|
+
{ sessionId: "g1", chatName: "group-chat", chatId: "oc_group1", chatType: "group", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
409
|
+
{ sessionId: "p1", chatName: "private-chat", chatId: "oc_private1", chatType: "p2p", active: false, turnCount: 1, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
410
|
+
]);
|
|
411
|
+
const content: string = JSON.parse(card).elements[0].text.content;
|
|
412
|
+
expect(content).toContain("(群聊)");
|
|
413
|
+
expect(content).toContain("(私聊)");
|
|
413
414
|
// 群聊会话包含 (群聊)
|
|
414
415
|
expect(content).toMatch(/group-chat.*\(群聊\)/);
|
|
415
416
|
// 私聊会话不包含 (群聊)
|
|
416
417
|
expect(content).toMatch(/private-chat/);
|
|
417
|
-
const afterPrivateChat = content.split("private-chat")[1];
|
|
418
|
-
expect(afterPrivateChat).not.toContain("(群聊)");
|
|
418
|
+
const afterPrivateChat = content.split("private-chat")[1];
|
|
419
|
+
expect(afterPrivateChat).not.toContain("(群聊)");
|
|
419
420
|
});
|
|
420
421
|
|
|
421
422
|
it("shows chat id missing for sessions without a chat binding", () => {
|
|
@@ -426,13 +427,23 @@ describe("buildSessionsCard", () => {
|
|
|
426
427
|
expect(content).toContain("chat id缺失");
|
|
427
428
|
});
|
|
428
429
|
|
|
429
|
-
it("includes /session help text in non-empty card", () => {
|
|
430
|
+
it("includes /session help text in non-empty card", () => {
|
|
430
431
|
const card = buildSessionsCard([
|
|
431
432
|
{ sessionId: "abc123", chatName: "", chatId: "oc_abc123", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
432
433
|
]);
|
|
433
434
|
const parsed = JSON.parse(card);
|
|
434
|
-
expect(parsed.elements[2].text.content).toContain("/session 数字");
|
|
435
|
-
});
|
|
435
|
+
expect(parsed.elements[2].text.content).toContain("/session 数字");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("explains fixed Feishu private-session behavior without advertising switching", () => {
|
|
439
|
+
const card = buildSessionsCard([
|
|
440
|
+
{ sessionId: "abc123", chatName: "飞书私聊", chatId: "ou_private", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
441
|
+
], { fixedPrivateSession: true });
|
|
442
|
+
const parsed = JSON.parse(card);
|
|
443
|
+
expect(parsed.elements[2].text.content).toContain("固定的专属会话");
|
|
444
|
+
expect(parsed.elements[2].text.content).toContain("不支持 **/session** 切换");
|
|
445
|
+
expect(parsed.elements[2].text.content).not.toContain("/session 数字");
|
|
446
|
+
});
|
|
436
447
|
|
|
437
448
|
it("includes close button", () => {
|
|
438
449
|
const card = buildSessionsCard([]);
|
|
@@ -446,7 +457,7 @@ describe("buildSessionsCard", () => {
|
|
|
446
457
|
// buildStatusCard
|
|
447
458
|
// ---------------------------------------------------------------------------
|
|
448
459
|
|
|
449
|
-
describe("buildStatusCard", () => {
|
|
460
|
+
describe("buildStatusCard", () => {
|
|
450
461
|
it("returns valid JSON with status text", () => {
|
|
451
462
|
const card = buildStatusCard("一切正常");
|
|
452
463
|
const parsed = JSON.parse(card);
|
|
@@ -466,54 +477,54 @@ describe("buildStatusCard", () => {
|
|
|
466
477
|
const action = parsed.elements[2];
|
|
467
478
|
expect(action.actions[0].text.content).toBe("收起");
|
|
468
479
|
});
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
// buildCodexUsageCard / buildCodexResetConfirmCard
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
describe("Codex usage reset cards", () => {
|
|
487
|
+
it("shows the reset button only when reset credits are available", () => {
|
|
488
|
+
const card = buildCodexUsageCard("Codex 用量", 2);
|
|
489
|
+
const parsed = JSON.parse(card);
|
|
490
|
+
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
491
|
+
expect(action.actions[0].text.content).toBe("发起重置");
|
|
492
|
+
expect(action.actions[0].value).toEqual({ action: "codex_reset_request", availableCount: 2 });
|
|
493
|
+
|
|
494
|
+
const noCreditCard = buildCodexUsageCard("Codex 用量", 0);
|
|
495
|
+
const noCreditParsed = JSON.parse(noCreditCard);
|
|
496
|
+
expect(noCreditParsed.elements.some((element: any) => element.tag === "action")).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("builds yes/no confirmation buttons tied to the parent usage card", () => {
|
|
500
|
+
const card = buildCodexResetConfirmCard({
|
|
501
|
+
availableCount: 2,
|
|
502
|
+
parentMessageId: "usage-message",
|
|
503
|
+
requestId: "request-1",
|
|
504
|
+
});
|
|
505
|
+
const parsed = JSON.parse(card);
|
|
506
|
+
const action = parsed.elements.find((element: any) => element.tag === "action");
|
|
507
|
+
|
|
508
|
+
expect(action.actions[0].text.content).toBe("是,发起重置");
|
|
509
|
+
expect(action.actions[0].value).toEqual({
|
|
510
|
+
action: "codex_reset_confirm",
|
|
511
|
+
decision: "yes",
|
|
512
|
+
parentMessageId: "usage-message",
|
|
513
|
+
requestId: "request-1",
|
|
514
|
+
});
|
|
515
|
+
expect(action.actions[1].text.content).toBe("否");
|
|
516
|
+
expect(action.actions[1].value).toEqual({
|
|
517
|
+
action: "codex_reset_confirm",
|
|
518
|
+
decision: "no",
|
|
519
|
+
parentMessageId: "usage-message",
|
|
520
|
+
requestId: "request-1",
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// buildButtons
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
517
528
|
|
|
518
529
|
describe("buildButtons", () => {
|
|
519
530
|
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
|
+
});
|