chatccc 0.2.226 → 0.2.227
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -76
- package/src/__tests__/builtin-chat-session.test.ts +350 -350
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +275 -275
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-skills.test.ts +185 -74
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/ccc-adapter.test.ts +136 -136
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/progress-reducer.test.ts +110 -110
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +18 -18
- package/src/adapters/ccc-adapter.ts +131 -131
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/agent-activity.ts +170 -170
- package/src/agent-delegate-task.ts +91 -91
- package/src/builtin/cli.ts +61 -2
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/index.ts +44 -24
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +108 -108
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/skills.ts +126 -44
- package/src/card-action-routing.ts +14 -14
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/index.ts +306 -306
- package/src/orchestrator.ts +2388 -2388
- package/src/platform-adapter.ts +6 -6
- package/src/progress/reducer.ts +108 -108
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/session-chat-binding.ts +82 -82
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +18 -18
- package/src/update-command-guard.ts +165 -165
|
@@ -1,40 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, utimesSync } from "node:fs";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
6
|
import {
|
|
8
|
-
buildSkillsIndexPrompt,
|
|
9
7
|
parseSkillFrontmatter,
|
|
10
8
|
scanSkillsDirs,
|
|
9
|
+
buildDefaultSkillDirs,
|
|
10
|
+
buildSkillsIndexPrompt,
|
|
11
|
+
buildSkillTemplate,
|
|
12
|
+
type SkillDirSpec,
|
|
13
|
+
type SkillSource,
|
|
14
|
+
type SkillScope,
|
|
11
15
|
} from "../builtin/skills.ts";
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
let tempRoot: string;
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return dir;
|
|
19
|
-
}
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tempRoot = mkdtempSync(join(tmpdir(), "deepccc-skills-"));
|
|
21
|
+
});
|
|
20
22
|
|
|
21
|
-
afterEach(
|
|
22
|
-
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
try {
|
|
25
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
26
|
+
} catch {}
|
|
23
27
|
});
|
|
24
28
|
|
|
29
|
+
function makeSkill(specDir: string, name: string, description: string): string {
|
|
30
|
+
const dir = join(specDir, name);
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
const skillPath = join(dir, "SKILL.md");
|
|
33
|
+
writeFileSync(skillPath, `---\nname: ${name}\ndescription: ${description}\n---\n\nbody\n`, "utf8");
|
|
34
|
+
return skillPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function spec(
|
|
38
|
+
dir: string,
|
|
39
|
+
source: SkillSource,
|
|
40
|
+
scope: SkillScope = "global",
|
|
41
|
+
): SkillDirSpec {
|
|
42
|
+
return { dir, source, scope };
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
describe("parseSkillFrontmatter", () => {
|
|
26
46
|
it("parses name and description from frontmatter", () => {
|
|
27
47
|
const content = [
|
|
28
48
|
"---",
|
|
29
49
|
"name: feishu-doc-download-md",
|
|
30
|
-
"description:
|
|
50
|
+
"description: 涓嬭浇椋炰功鏂囨。涓?Markdown",
|
|
31
51
|
"---",
|
|
32
52
|
"",
|
|
33
|
-
"#
|
|
53
|
+
"# 姝f枃",
|
|
34
54
|
].join("\n");
|
|
35
55
|
expect(parseSkillFrontmatter(content)).toEqual({
|
|
36
56
|
name: "feishu-doc-download-md",
|
|
37
|
-
description: "
|
|
57
|
+
description: "涓嬭浇椋炰功鏂囨。涓?Markdown",
|
|
38
58
|
});
|
|
39
59
|
});
|
|
40
60
|
|
|
@@ -50,88 +70,164 @@ describe("parseSkillFrontmatter", () => {
|
|
|
50
70
|
});
|
|
51
71
|
|
|
52
72
|
it("handles CRLF line endings", () => {
|
|
53
|
-
const content = "---\r\nname: crlf-skill\r\ndescription: CRLF
|
|
73
|
+
const content = "---\r\nname: crlf-skill\r\ndescription: CRLF 鎻忚堪\r\n---\r\n\r\nbody";
|
|
54
74
|
expect(parseSkillFrontmatter(content)).toEqual({
|
|
55
75
|
name: "crlf-skill",
|
|
56
|
-
description: "CRLF
|
|
76
|
+
description: "CRLF 鎻忚堪",
|
|
57
77
|
});
|
|
58
78
|
});
|
|
59
79
|
});
|
|
60
80
|
|
|
61
|
-
describe("scanSkillsDirs", () => {
|
|
62
|
-
it("
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
join(project, "feishu-doc", "SKILL.md"),
|
|
96
|
-
"---\nname: feishu-doc\ndescription: 项目级覆盖\n---\n",
|
|
97
|
-
"utf8",
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const skills = scanSkillsDirs([userA, userB, project]);
|
|
101
|
-
|
|
102
|
-
// 去重:同名保留一个,后面的目录(项目级)覆盖前面的(用户级)
|
|
103
|
-
expect(skills).toHaveLength(2);
|
|
81
|
+
describe("scanSkillsDirs priority matrix", () => {
|
|
82
|
+
it("codex wins over cursor, cursor wins over claude for same-name skills", async () => {
|
|
83
|
+
const claudeDir = join(tempRoot, "claude");
|
|
84
|
+
const cursorDir = join(tempRoot, "cursor");
|
|
85
|
+
const codexDir = join(tempRoot, "codex");
|
|
86
|
+
makeSkill(claudeDir, "dupe", "claude version");
|
|
87
|
+
makeSkill(cursorDir, "dupe", "cursor version");
|
|
88
|
+
makeSkill(codexDir, "dupe", "codex version");
|
|
89
|
+
makeSkill(codexDir, "only-codex", "codex only");
|
|
90
|
+
|
|
91
|
+
const skills = await scanSkillsDirs([
|
|
92
|
+
spec(claudeDir, "claude"),
|
|
93
|
+
spec(cursorDir, "cursor"),
|
|
94
|
+
spec(codexDir, "codex"),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const byName = new Map(skills.map((s) => [s.name, s]));
|
|
98
|
+
expect(byName.get("dupe")?.description).toBe("codex version");
|
|
99
|
+
expect(byName.get("dupe")?.source).toBe("codex");
|
|
100
|
+
expect(byName.get("dupe")?.skillPath).toContain(join("codex", "dupe"));
|
|
101
|
+
expect(byName.get("only-codex")?.source).toBe("codex");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("project scope wins over global scope within the same source", async () => {
|
|
105
|
+
const globalCodex = join(tempRoot, "codex-global");
|
|
106
|
+
const projectCodex = join(tempRoot, "codex-project");
|
|
107
|
+
makeSkill(globalCodex, "dup", "global version");
|
|
108
|
+
makeSkill(projectCodex, "dup", "project version");
|
|
109
|
+
|
|
110
|
+
const skills = await scanSkillsDirs([
|
|
111
|
+
spec(globalCodex, "codex", "global"),
|
|
112
|
+
spec(projectCodex, "codex", "project"),
|
|
113
|
+
]);
|
|
114
|
+
|
|
104
115
|
const byName = new Map(skills.map((s) => [s.name, s]));
|
|
105
|
-
expect(byName.get("
|
|
106
|
-
expect(byName.get("
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
expect(byName.get("dup")?.description).toBe("project version");
|
|
117
|
+
expect(byName.get("dup")?.scope).toBe("project");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("deepccc source has the highest priority over codex", async () => {
|
|
121
|
+
const codexDir = join(tempRoot, "codex");
|
|
122
|
+
const deepcccDir = join(tempRoot, "deepccc");
|
|
123
|
+
makeSkill(codexDir, "dup", "from codex");
|
|
124
|
+
makeSkill(deepcccDir, "dup", "from deepccc");
|
|
125
|
+
|
|
126
|
+
const skills = await scanSkillsDirs([
|
|
127
|
+
spec(codexDir, "codex"),
|
|
128
|
+
spec(deepcccDir, "deepccc"),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(skills.find((s) => s.name === "dup")?.description).toBe("from deepccc");
|
|
132
|
+
expect(skills.find((s) => s.name === "dup")?.source).toBe("deepccc");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("skips hidden dirs, dirs without SKILL.md, and missing dirs", async () => {
|
|
136
|
+
const dir = join(tempRoot, "src");
|
|
137
|
+
mkdirSync(join(dir, ".system"), { recursive: true });
|
|
138
|
+
writeFileSync(join(dir, ".system", "SKILL.md"), "---\nname: system-skill\ndescription: x\n---\n", "utf8");
|
|
139
|
+
mkdirSync(join(dir, "no-skill-dir"));
|
|
140
|
+
|
|
141
|
+
makeSkill(dir, "ok", "fine");
|
|
142
|
+
const skills = await scanSkillsDirs([
|
|
143
|
+
spec(join(tempRoot, "missing-dir"), "codex"),
|
|
144
|
+
spec(dir, "codex"),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
expect(skills).toHaveLength(1);
|
|
148
|
+
expect(skills[0].name).toBe("ok");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("scanSkillsDirs hot reload (mtime cache)", () => {
|
|
153
|
+
it("re-reads a skill after its SKILL.md content changes", async () => {
|
|
154
|
+
const dir = join(tempRoot, "src");
|
|
155
|
+
const skillPath = makeSkill(dir, "live", "v1");
|
|
156
|
+
|
|
157
|
+
const first = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
158
|
+
expect(first.find((s) => s.name === "live")?.description).toBe("v1");
|
|
159
|
+
|
|
160
|
+
writeFileSync(skillPath, "---\nname: live\ndescription: v2\n---\n\nbody\n", "utf8");
|
|
161
|
+
utimesSync(skillPath, new Date(Date.now() + 3000), new Date(Date.now() + 3000)); // 寮哄埗 mtime 鍓嶈繘
|
|
162
|
+
|
|
163
|
+
const second = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
164
|
+
expect(second.find((s) => s.name === "live")?.description).toBe("v2");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("new skill dirs are picked up on the next scan", async () => {
|
|
168
|
+
const dir = join(tempRoot, "src");
|
|
169
|
+
makeSkill(dir, "a", "A");
|
|
170
|
+
|
|
171
|
+
const first = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
172
|
+
expect(first).toHaveLength(1);
|
|
173
|
+
|
|
174
|
+
makeSkill(dir, "b", "B"); // 鏂版妧鑳斤紝鏃犻渶鏀?mtime锛堢洰褰曟灇涓炬瘡娆¢兘鍋氾級
|
|
175
|
+
const second = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
176
|
+
expect(second.map((s) => s.name).sort()).toEqual(["a", "b"]);
|
|
109
177
|
});
|
|
110
178
|
|
|
111
|
-
it("
|
|
112
|
-
const dir =
|
|
113
|
-
|
|
179
|
+
it("unchanged skills return identical results across scans", async () => {
|
|
180
|
+
const dir = join(tempRoot, "src");
|
|
181
|
+
makeSkill(dir, "a", "A");
|
|
182
|
+
|
|
183
|
+
const first = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
184
|
+
const second = await scanSkillsDirs([spec(dir, "deepccc")]);
|
|
185
|
+
expect(second).toEqual(first);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
114
188
|
|
|
115
|
-
|
|
189
|
+
describe("buildDefaultSkillDirs", () => {
|
|
190
|
+
it("orders dirs low->high priority: claude < cursor < codex < deepccc, project after global", () => {
|
|
191
|
+
const dirs = buildDefaultSkillDirs("C:/proj");
|
|
192
|
+
expect(dirs.map((d) => `${d.source}:${d.scope}`)).toEqual([
|
|
193
|
+
"claude:global",
|
|
194
|
+
"claude:project",
|
|
195
|
+
"cursor:global",
|
|
196
|
+
"cursor:project",
|
|
197
|
+
"codex:global",
|
|
198
|
+
"codex:global",
|
|
199
|
+
"codex:project",
|
|
200
|
+
"deepccc:global",
|
|
201
|
+
"deepccc:project",
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
116
204
|
|
|
117
|
-
|
|
205
|
+
it("points codex global dirs at ~/.codex/skills and ~/.agents/skills", () => {
|
|
206
|
+
const dirs = buildDefaultSkillDirs("C:/proj").filter((d) => d.source === "codex" && d.scope === "global");
|
|
207
|
+
expect(dirs.map((d) => d.dir)).toEqual([
|
|
208
|
+
join(require("node:os").homedir(), ".codex", "skills"),
|
|
209
|
+
join(require("node:os").homedir(), ".agents", "skills"),
|
|
210
|
+
]);
|
|
118
211
|
});
|
|
119
212
|
});
|
|
120
213
|
|
|
121
214
|
describe("buildSkillsIndexPrompt", () => {
|
|
122
|
-
it("renders
|
|
215
|
+
it("renders index with source markers and the skill creation convention", () => {
|
|
123
216
|
const prompt = buildSkillsIndexPrompt([
|
|
124
217
|
{
|
|
125
218
|
name: "feishu-doc",
|
|
126
|
-
description: "
|
|
127
|
-
skillPath: "C
|
|
219
|
+
description: "涓嬭浇椋炰功鏂囨。",
|
|
220
|
+
skillPath: "C:/x/feishu-doc/SKILL.md",
|
|
221
|
+
source: "codex",
|
|
222
|
+
scope: "global",
|
|
128
223
|
},
|
|
129
224
|
]);
|
|
130
225
|
|
|
131
226
|
expect(prompt).toContain("## Available Skills");
|
|
132
227
|
expect(prompt).toContain("**feishu-doc**");
|
|
133
|
-
expect(prompt).toContain("
|
|
134
|
-
expect(prompt).toContain("
|
|
228
|
+
expect(prompt).toContain("[codex:global]");
|
|
229
|
+
expect(prompt).toContain("## Creating Skills");
|
|
230
|
+
expect(prompt).toContain(".deepccc/skills");
|
|
135
231
|
expect(prompt).toContain("read_file");
|
|
136
232
|
});
|
|
137
233
|
|
|
@@ -139,3 +235,18 @@ describe("buildSkillsIndexPrompt", () => {
|
|
|
139
235
|
expect(buildSkillsIndexPrompt([])).toBe("");
|
|
140
236
|
});
|
|
141
237
|
});
|
|
238
|
+
|
|
239
|
+
describe("buildSkillTemplate", () => {
|
|
240
|
+
it("renders a Codex-style SKILL.md with frontmatter", () => {
|
|
241
|
+
const tpl = buildSkillTemplate("my-skill", "does something");
|
|
242
|
+
expect(tpl.startsWith("---")).toBe(true);
|
|
243
|
+
expect(tpl).toContain("name: my-skill");
|
|
244
|
+
expect(tpl).toContain("description: does something");
|
|
245
|
+
expect(tpl).toContain("# my-skill");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("defaults description to empty when omitted", () => {
|
|
249
|
+
const tpl = buildSkillTemplate("bare-skill", "");
|
|
250
|
+
expect(tpl).toContain("description: ");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { resolveFeishuCardActionChatType } from "../card-action-routing.ts";
|
|
4
|
-
|
|
5
|
-
describe("resolveFeishuCardActionChatType", () => {
|
|
6
|
-
it("keeps card commands in a persisted private chat on the p2p route", () => {
|
|
7
|
-
expect(resolveFeishuCardActionChatType("private-chat", {
|
|
8
|
-
"private-chat": { chatType: "p2p" },
|
|
9
|
-
})).toBe("p2p");
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it("defaults unknown and group chats to the group route", () => {
|
|
13
|
-
expect(resolveFeishuCardActionChatType("group-chat", {
|
|
14
|
-
"group-chat": { chatType: "group" },
|
|
15
|
-
})).toBe("group");
|
|
16
|
-
expect(resolveFeishuCardActionChatType("unknown-chat", {})).toBe("group");
|
|
17
|
-
});
|
|
18
|
-
});
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { resolveFeishuCardActionChatType } from "../card-action-routing.ts";
|
|
4
|
+
|
|
5
|
+
describe("resolveFeishuCardActionChatType", () => {
|
|
6
|
+
it("keeps card commands in a persisted private chat on the p2p route", () => {
|
|
7
|
+
expect(resolveFeishuCardActionChatType("private-chat", {
|
|
8
|
+
"private-chat": { chatType: "p2p" },
|
|
9
|
+
})).toBe("p2p");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("defaults unknown and group chats to the group route", () => {
|
|
13
|
+
expect(resolveFeishuCardActionChatType("group-chat", {
|
|
14
|
+
"group-chat": { chatType: "group" },
|
|
15
|
+
})).toBe("group");
|
|
16
|
+
expect(resolveFeishuCardActionChatType("unknown-chat", {})).toBe("group");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -1,136 +1,136 @@
|
|
|
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
|
-
{ type: "assistant", blocks: [], isFinalResponse: true },
|
|
77
|
-
]);
|
|
78
|
-
expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
79
|
-
model: { modelId: "deepseek-v4-flash" },
|
|
80
|
-
}));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("maps ChatSession tool events to unified tool blocks", async () => {
|
|
84
|
-
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
85
|
-
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
|
|
86
|
-
const adapter = createCccAdapter({
|
|
87
|
-
apiKey: "sk-test",
|
|
88
|
-
contextDir,
|
|
89
|
-
model: "deepseek-v4-flash",
|
|
90
|
-
});
|
|
91
|
-
const { sessionId } = await adapter.createSession("F:\\repo");
|
|
92
|
-
streamTextMock.mockReturnValueOnce({
|
|
93
|
-
fullStream: fullStream(
|
|
94
|
-
{ type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
|
|
95
|
-
{ type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
|
|
96
|
-
),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const messages = [];
|
|
100
|
-
for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
|
|
101
|
-
messages.push(message);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
expect(messages).toEqual([
|
|
105
|
-
{
|
|
106
|
-
type: "assistant",
|
|
107
|
-
blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
type: "assistant",
|
|
111
|
-
blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
|
|
112
|
-
},
|
|
113
|
-
{ type: "assistant", blocks: [], isFinalResponse: true },
|
|
114
|
-
]);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("passes effort into ChatSession so streamText receives reasoningEffort", async () => {
|
|
118
|
-
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
119
|
-
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-effort-"));
|
|
120
|
-
const adapter = createCccAdapter({
|
|
121
|
-
apiKey: "sk-test",
|
|
122
|
-
contextDir,
|
|
123
|
-
effort: "xhigh",
|
|
124
|
-
});
|
|
125
|
-
const { sessionId } = await adapter.createSession("F:\\repo");
|
|
126
|
-
streamTextMock.mockReturnValueOnce({ textStream: textStream("ok") });
|
|
127
|
-
|
|
128
|
-
for await (const _message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
|
|
129
|
-
// drain
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
133
|
-
providerOptions: { deepseek: { reasoningEffort: "xhigh" } },
|
|
134
|
-
}));
|
|
135
|
-
});
|
|
136
|
-
});
|
|
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
|
+
{ type: "assistant", blocks: [], isFinalResponse: true },
|
|
77
|
+
]);
|
|
78
|
+
expect(streamTextMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
79
|
+
model: { modelId: "deepseek-v4-flash" },
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("maps ChatSession tool events to unified tool blocks", async () => {
|
|
84
|
+
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
85
|
+
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-tools-"));
|
|
86
|
+
const adapter = createCccAdapter({
|
|
87
|
+
apiKey: "sk-test",
|
|
88
|
+
contextDir,
|
|
89
|
+
model: "deepseek-v4-flash",
|
|
90
|
+
});
|
|
91
|
+
const { sessionId } = await adapter.createSession("F:\\repo");
|
|
92
|
+
streamTextMock.mockReturnValueOnce({
|
|
93
|
+
fullStream: fullStream(
|
|
94
|
+
{ type: "tool-call", toolCallId: "call-1", toolName: "read_file", input: { path: "README.md" } },
|
|
95
|
+
{ type: "tool-result", toolCallId: "call-1", toolName: "read_file", output: { content: "hello" } },
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const messages = [];
|
|
100
|
+
for await (const message of adapter.prompt(sessionId, "read", "F:\\repo")) {
|
|
101
|
+
messages.push(message);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
expect(messages).toEqual([
|
|
105
|
+
{
|
|
106
|
+
type: "assistant",
|
|
107
|
+
blocks: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "README.md" } }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "assistant",
|
|
111
|
+
blocks: [{ type: "tool_result", tool_use_id: "call-1", content: { content: "hello" }, is_error: false }],
|
|
112
|
+
},
|
|
113
|
+
{ type: "assistant", blocks: [], isFinalResponse: true },
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("passes effort into ChatSession so streamText receives reasoningEffort", async () => {
|
|
118
|
+
const { createCccAdapter } = await import("../adapters/ccc-adapter.ts");
|
|
119
|
+
const contextDir = await mkdtemp(join(tmpdir(), "chatccc-ccc-adapter-effort-"));
|
|
120
|
+
const adapter = createCccAdapter({
|
|
121
|
+
apiKey: "sk-test",
|
|
122
|
+
contextDir,
|
|
123
|
+
effort: "xhigh",
|
|
124
|
+
});
|
|
125
|
+
const { sessionId } = await adapter.createSession("F:\\repo");
|
|
126
|
+
streamTextMock.mockReturnValueOnce({ textStream: textStream("ok") });
|
|
127
|
+
|
|
128
|
+
for await (const _message of adapter.prompt(sessionId, "hi", "F:\\repo")) {
|
|
129
|
+
// drain
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
expect(streamTextMock).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
133
|
+
providerOptions: { deepseek: { reasoningEffort: "xhigh" } },
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
});
|