chatccc 0.2.225 → 0.2.226
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 -37
- 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 -0
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-skills.test.ts +141 -141
- 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 -128
- 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 +598 -556
- package/src/builtin/config.ts +84 -0
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-log.ts +38 -0
- package/src/builtin/file-tools.ts +1407 -1320
- package/src/builtin/index.ts +437 -426
- package/src/builtin/permissions.ts +226 -0
- package/src/builtin/privacy.ts +141 -0
- package/src/builtin/proc-tree-kill.ts +61 -0
- package/src/builtin/progress/cards-helpers.ts +76 -0
- package/src/builtin/progress/reducer.ts +108 -0
- package/src/builtin/progress/terminal-renderer.ts +294 -0
- package/src/builtin/progress/view.ts +77 -0
- package/src/builtin/raw-stream-log.ts +124 -0
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/skills.ts +108 -108
- 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,141 +1,141 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
buildSkillsIndexPrompt,
|
|
9
|
-
parseSkillFrontmatter,
|
|
10
|
-
scanSkillsDirs,
|
|
11
|
-
} from "../builtin/skills.ts";
|
|
12
|
-
|
|
13
|
-
const tempDirs: string[] = [];
|
|
14
|
-
|
|
15
|
-
async function makeTempDir(): Promise<string> {
|
|
16
|
-
const dir = await mkdtemp(join(tmpdir(), "chatccc-skills-"));
|
|
17
|
-
tempDirs.push(dir);
|
|
18
|
-
return dir;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("parseSkillFrontmatter", () => {
|
|
26
|
-
it("parses name and description from frontmatter", () => {
|
|
27
|
-
const content = [
|
|
28
|
-
"---",
|
|
29
|
-
"name: feishu-doc-download-md",
|
|
30
|
-
"description: 下载飞书文档为 Markdown",
|
|
31
|
-
"---",
|
|
32
|
-
"",
|
|
33
|
-
"# 正文",
|
|
34
|
-
].join("\n");
|
|
35
|
-
expect(parseSkillFrontmatter(content)).toEqual({
|
|
36
|
-
name: "feishu-doc-download-md",
|
|
37
|
-
description: "下载飞书文档为 Markdown",
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns null when there is no frontmatter", () => {
|
|
42
|
-
expect(parseSkillFrontmatter("# just a heading")).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("tolerates missing description", () => {
|
|
46
|
-
expect(parseSkillFrontmatter("---\nname: minimal-skill\n---\n\nbody")).toEqual({
|
|
47
|
-
name: "minimal-skill",
|
|
48
|
-
description: "",
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("handles CRLF line endings", () => {
|
|
53
|
-
const content = "---\r\nname: crlf-skill\r\ndescription: CRLF 描述\r\n---\r\n\r\nbody";
|
|
54
|
-
expect(parseSkillFrontmatter(content)).toEqual({
|
|
55
|
-
name: "crlf-skill",
|
|
56
|
-
description: "CRLF 描述",
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe("scanSkillsDirs", () => {
|
|
62
|
-
it("scans multiple dirs, skips hidden/system dirs, dedupes by name (later dir wins)", async () => {
|
|
63
|
-
const dir = await makeTempDir();
|
|
64
|
-
const userA = join(dir, "codex-skills");
|
|
65
|
-
const userB = join(dir, "agents-skills");
|
|
66
|
-
const project = join(dir, "project-codex");
|
|
67
|
-
await mkdir(join(userA, "feishu-doc"), { recursive: true });
|
|
68
|
-
await mkdir(join(userA, ".system"), { recursive: true });
|
|
69
|
-
await mkdir(join(userA, "no-skill-dir"));
|
|
70
|
-
await mkdir(join(userB, "feishu-doc"), { recursive: true });
|
|
71
|
-
await mkdir(join(userB, "another"), { recursive: true });
|
|
72
|
-
await mkdir(join(project, "feishu-doc"), { recursive: true });
|
|
73
|
-
|
|
74
|
-
await writeFile(
|
|
75
|
-
join(userA, "feishu-doc", "SKILL.md"),
|
|
76
|
-
"---\nname: feishu-doc\ndescription: 用户级 A\n---\n",
|
|
77
|
-
"utf8",
|
|
78
|
-
);
|
|
79
|
-
await writeFile(
|
|
80
|
-
join(userA, ".system", "SKILL.md"),
|
|
81
|
-
"---\nname: system-skill\ndescription: 内置\n---\n",
|
|
82
|
-
"utf8",
|
|
83
|
-
);
|
|
84
|
-
await writeFile(
|
|
85
|
-
join(userB, "feishu-doc", "SKILL.md"),
|
|
86
|
-
"---\nname: feishu-doc\ndescription: 用户级 B\n---\n",
|
|
87
|
-
"utf8",
|
|
88
|
-
);
|
|
89
|
-
await writeFile(
|
|
90
|
-
join(userB, "another", "SKILL.md"),
|
|
91
|
-
"---\nname: another\ndescription: 另一个\n---\n",
|
|
92
|
-
"utf8",
|
|
93
|
-
);
|
|
94
|
-
await writeFile(
|
|
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);
|
|
104
|
-
const byName = new Map(skills.map((s) => [s.name, s]));
|
|
105
|
-
expect(byName.get("feishu-doc")?.description).toBe("项目级覆盖");
|
|
106
|
-
expect(byName.get("feishu-doc")?.skillPath).toBe(join(project, "feishu-doc", "SKILL.md"));
|
|
107
|
-
expect(byName.get("another")?.description).toBe("另一个");
|
|
108
|
-
expect(byName.has("system-skill")).toBe(false); // 隐藏目录被排除
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("skips missing directories and dirs without SKILL.md", async () => {
|
|
112
|
-
const dir = await makeTempDir();
|
|
113
|
-
await mkdir(join(dir, "empty-dir"));
|
|
114
|
-
|
|
115
|
-
const skills = scanSkillsDirs([join(dir, "missing-dir"), join(dir, "empty-dir")]);
|
|
116
|
-
|
|
117
|
-
expect(skills).toEqual([]);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("buildSkillsIndexPrompt", () => {
|
|
122
|
-
it("renders a skill index with names, paths and descriptions", () => {
|
|
123
|
-
const prompt = buildSkillsIndexPrompt([
|
|
124
|
-
{
|
|
125
|
-
name: "feishu-doc",
|
|
126
|
-
description: "下载飞书文档",
|
|
127
|
-
skillPath: "C:\\users\\x\\skills\\feishu-doc\\SKILL.md",
|
|
128
|
-
},
|
|
129
|
-
]);
|
|
130
|
-
|
|
131
|
-
expect(prompt).toContain("## Available Skills");
|
|
132
|
-
expect(prompt).toContain("**feishu-doc**");
|
|
133
|
-
expect(prompt).toContain("下载飞书文档");
|
|
134
|
-
expect(prompt).toContain("C:\\users\\x\\skills\\feishu-doc\\SKILL.md");
|
|
135
|
-
expect(prompt).toContain("read_file");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("returns empty string for no skills", () => {
|
|
139
|
-
expect(buildSkillsIndexPrompt([])).toBe("");
|
|
140
|
-
});
|
|
141
|
-
});
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildSkillsIndexPrompt,
|
|
9
|
+
parseSkillFrontmatter,
|
|
10
|
+
scanSkillsDirs,
|
|
11
|
+
} from "../builtin/skills.ts";
|
|
12
|
+
|
|
13
|
+
const tempDirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
async function makeTempDir(): Promise<string> {
|
|
16
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-skills-"));
|
|
17
|
+
tempDirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("parseSkillFrontmatter", () => {
|
|
26
|
+
it("parses name and description from frontmatter", () => {
|
|
27
|
+
const content = [
|
|
28
|
+
"---",
|
|
29
|
+
"name: feishu-doc-download-md",
|
|
30
|
+
"description: 下载飞书文档为 Markdown",
|
|
31
|
+
"---",
|
|
32
|
+
"",
|
|
33
|
+
"# 正文",
|
|
34
|
+
].join("\n");
|
|
35
|
+
expect(parseSkillFrontmatter(content)).toEqual({
|
|
36
|
+
name: "feishu-doc-download-md",
|
|
37
|
+
description: "下载飞书文档为 Markdown",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns null when there is no frontmatter", () => {
|
|
42
|
+
expect(parseSkillFrontmatter("# just a heading")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("tolerates missing description", () => {
|
|
46
|
+
expect(parseSkillFrontmatter("---\nname: minimal-skill\n---\n\nbody")).toEqual({
|
|
47
|
+
name: "minimal-skill",
|
|
48
|
+
description: "",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles CRLF line endings", () => {
|
|
53
|
+
const content = "---\r\nname: crlf-skill\r\ndescription: CRLF 描述\r\n---\r\n\r\nbody";
|
|
54
|
+
expect(parseSkillFrontmatter(content)).toEqual({
|
|
55
|
+
name: "crlf-skill",
|
|
56
|
+
description: "CRLF 描述",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("scanSkillsDirs", () => {
|
|
62
|
+
it("scans multiple dirs, skips hidden/system dirs, dedupes by name (later dir wins)", async () => {
|
|
63
|
+
const dir = await makeTempDir();
|
|
64
|
+
const userA = join(dir, "codex-skills");
|
|
65
|
+
const userB = join(dir, "agents-skills");
|
|
66
|
+
const project = join(dir, "project-codex");
|
|
67
|
+
await mkdir(join(userA, "feishu-doc"), { recursive: true });
|
|
68
|
+
await mkdir(join(userA, ".system"), { recursive: true });
|
|
69
|
+
await mkdir(join(userA, "no-skill-dir"));
|
|
70
|
+
await mkdir(join(userB, "feishu-doc"), { recursive: true });
|
|
71
|
+
await mkdir(join(userB, "another"), { recursive: true });
|
|
72
|
+
await mkdir(join(project, "feishu-doc"), { recursive: true });
|
|
73
|
+
|
|
74
|
+
await writeFile(
|
|
75
|
+
join(userA, "feishu-doc", "SKILL.md"),
|
|
76
|
+
"---\nname: feishu-doc\ndescription: 用户级 A\n---\n",
|
|
77
|
+
"utf8",
|
|
78
|
+
);
|
|
79
|
+
await writeFile(
|
|
80
|
+
join(userA, ".system", "SKILL.md"),
|
|
81
|
+
"---\nname: system-skill\ndescription: 内置\n---\n",
|
|
82
|
+
"utf8",
|
|
83
|
+
);
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(userB, "feishu-doc", "SKILL.md"),
|
|
86
|
+
"---\nname: feishu-doc\ndescription: 用户级 B\n---\n",
|
|
87
|
+
"utf8",
|
|
88
|
+
);
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(userB, "another", "SKILL.md"),
|
|
91
|
+
"---\nname: another\ndescription: 另一个\n---\n",
|
|
92
|
+
"utf8",
|
|
93
|
+
);
|
|
94
|
+
await writeFile(
|
|
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);
|
|
104
|
+
const byName = new Map(skills.map((s) => [s.name, s]));
|
|
105
|
+
expect(byName.get("feishu-doc")?.description).toBe("项目级覆盖");
|
|
106
|
+
expect(byName.get("feishu-doc")?.skillPath).toBe(join(project, "feishu-doc", "SKILL.md"));
|
|
107
|
+
expect(byName.get("another")?.description).toBe("另一个");
|
|
108
|
+
expect(byName.has("system-skill")).toBe(false); // 隐藏目录被排除
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("skips missing directories and dirs without SKILL.md", async () => {
|
|
112
|
+
const dir = await makeTempDir();
|
|
113
|
+
await mkdir(join(dir, "empty-dir"));
|
|
114
|
+
|
|
115
|
+
const skills = scanSkillsDirs([join(dir, "missing-dir"), join(dir, "empty-dir")]);
|
|
116
|
+
|
|
117
|
+
expect(skills).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("buildSkillsIndexPrompt", () => {
|
|
122
|
+
it("renders a skill index with names, paths and descriptions", () => {
|
|
123
|
+
const prompt = buildSkillsIndexPrompt([
|
|
124
|
+
{
|
|
125
|
+
name: "feishu-doc",
|
|
126
|
+
description: "下载飞书文档",
|
|
127
|
+
skillPath: "C:\\users\\x\\skills\\feishu-doc\\SKILL.md",
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(prompt).toContain("## Available Skills");
|
|
132
|
+
expect(prompt).toContain("**feishu-doc**");
|
|
133
|
+
expect(prompt).toContain("下载飞书文档");
|
|
134
|
+
expect(prompt).toContain("C:\\users\\x\\skills\\feishu-doc\\SKILL.md");
|
|
135
|
+
expect(prompt).toContain("read_file");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("returns empty string for no skills", () => {
|
|
139
|
+
expect(buildSkillsIndexPrompt([])).toBe("");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -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
|
+
});
|