chatccc 0.2.63 → 0.2.64

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.
@@ -6,25 +6,25 @@ import { describe, expect, it } from "vitest";
6
6
  describe("config.sample.json", () => {
7
7
  it("enables Feishu and WeChat iLink by default", () => {
8
8
  const configSamplePath = join(process.cwd(), "config.sample.json");
9
- const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
10
- platforms?: {
11
- feishu?: { enabled?: unknown };
12
- ilink?: { enabled?: unknown };
13
- };
14
- claude?: { model?: unknown; subagentModel?: unknown };
15
- };
16
-
17
- expect(sample.platforms?.feishu?.enabled).toBe(true);
18
- expect(sample.platforms?.ilink?.enabled).toBe(true);
19
- });
20
-
21
- it("uses an Anthropic official main model and leaves subagent override empty by default", () => {
22
- const configSamplePath = join(process.cwd(), "config.sample.json");
23
- const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
24
- claude?: { model?: unknown; subagentModel?: unknown };
25
- };
26
-
27
- expect(sample.claude?.model).toBe("claude-sonnet-4-6");
28
- expect(sample.claude?.subagentModel).toBe("");
29
- });
30
- });
9
+ const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
10
+ platforms?: {
11
+ feishu?: { enabled?: unknown };
12
+ ilink?: { enabled?: unknown };
13
+ };
14
+ claude?: { model?: unknown; subagentModel?: unknown };
15
+ };
16
+
17
+ expect(sample.platforms?.feishu?.enabled).toBe(true);
18
+ expect(sample.platforms?.ilink?.enabled).toBe(true);
19
+ });
20
+
21
+ it("leaves Claude model and subagent model empty by default so the SDK uses its own defaults", () => {
22
+ const configSamplePath = join(process.cwd(), "config.sample.json");
23
+ const sample = JSON.parse(readFileSync(configSamplePath, "utf8")) as {
24
+ claude?: { model?: unknown; subagentModel?: unknown };
25
+ };
26
+
27
+ expect(sample.claude?.model).toBe("");
28
+ expect(sample.claude?.subagentModel).toBe("");
29
+ });
30
+ });
@@ -1,4 +1,4 @@
1
- import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { afterEach, describe, expect, it } from "vitest";
@@ -44,6 +44,60 @@ describe("IM skills prompt rendering", () => {
44
44
  expect(prompt).not.toContain("{{");
45
45
  });
46
46
 
47
+ it("filters skills by enabledSkillNames", async () => {
48
+ tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-filter-"));
49
+ const feishuDir = join(tempRoot, "feishu-skill");
50
+ const wechatDir = join(tempRoot, "wechat-skill");
51
+ const outDir = join(tempRoot, "out");
52
+ await mkdir(feishuDir);
53
+ await mkdir(wechatDir);
54
+ await writeFile(join(feishuDir, "skill.md"), "Feishu {{cwd}}", "utf-8");
55
+ await writeFile(join(feishuDir, "receive-send-image.md"), "Feishu doc", "utf-8");
56
+ await writeFile(join(wechatDir, "skill.md"), "WeChat {{cwd}}", "utf-8");
57
+ await writeFile(join(wechatDir, "receive-send-image.md"), "WeChat doc", "utf-8");
58
+
59
+ const input = {
60
+ skillsDir: tempRoot,
61
+ enabledSkillNames: ["feishu-skill"],
62
+ variables: { cwd: "C:/work" },
63
+ };
64
+ const prompt = await buildImSkillsPrompt(input);
65
+ const exported = await exportSkillSubDocs(input, outDir);
66
+
67
+ expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
68
+ expect(prompt).toContain("Feishu C:/work");
69
+ expect(prompt).not.toContain("[ChatCCC IM skill: wechat-skill]");
70
+ expect(prompt).not.toContain("WeChat C:/work");
71
+ expect(exported).toHaveLength(1);
72
+ expect(exported[0]).toContain(join("feishu-skill", "receive-send-image.md"));
73
+ await expect(readFile(join(outDir, "feishu-skill", "receive-send-image.md"), "utf8")).resolves.toContain(
74
+ "Feishu doc",
75
+ );
76
+ });
77
+
78
+ it("skips filtering when enabledSkillNames is omitted", async () => {
79
+ tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-all-"));
80
+ const feishuDir = join(tempRoot, "feishu-skill");
81
+ const wechatDir = join(tempRoot, "wechat-skill");
82
+ const outDir = join(tempRoot, "out");
83
+ await mkdir(feishuDir);
84
+ await mkdir(wechatDir);
85
+ await writeFile(join(feishuDir, "skill.md"), "Feishu {{cwd}}", "utf-8");
86
+ await writeFile(join(feishuDir, "receive-send-image.md"), "Feishu doc", "utf-8");
87
+ await writeFile(join(wechatDir, "skill.md"), "WeChat {{cwd}}", "utf-8");
88
+ await writeFile(join(wechatDir, "receive-send-image.md"), "WeChat doc", "utf-8");
89
+
90
+ const prompt = await buildImSkillsPrompt({
91
+ skillsDir: tempRoot,
92
+ variables: { cwd: "C:/work" },
93
+ });
94
+ const exported = await exportSkillSubDocs({ skillsDir: tempRoot, variables: { cwd: "C:/work" } }, outDir);
95
+
96
+ expect(prompt).toContain("[ChatCCC IM skill: feishu-skill]");
97
+ expect(prompt).toContain("[ChatCCC IM skill: wechat-skill]");
98
+ expect(exported.length).toBeGreaterThanOrEqual(2);
99
+ });
100
+
47
101
  it("renders separate WeChat image, file, and video capability docs", async () => {
48
102
  tempRoot = await mkdtemp(join(tmpdir(), "chatccc-im-skills-cache-"));
49
103
  const prompt = await buildImSkillsPrompt({
@@ -98,4 +152,4 @@ describe("IM skills prompt rendering", () => {
98
152
  "send-video.mjs",
99
153
  );
100
154
  });
101
- });
155
+ });
@@ -1,143 +1,143 @@
1
- import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- // 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
7
- const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
8
- vi.mock("../config.ts", async () => {
9
- const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
10
- return {
11
- ...actual,
12
- USER_DATA_DIR: TEST_DATA_DIR,
13
- ts: () => "test-ts",
14
- };
15
- });
16
-
17
- let applyPrivacy: (text: string) => string;
18
- let reloadPrivacyRules: () => void;
19
- let getPrivacyRules: () => Record<string, string>;
20
-
21
- beforeEach(async () => {
22
- vi.resetModules();
23
- // 清理临时目录中的 privacy.json
24
- try {
25
- await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
26
- } catch {}
27
- const mod = await import("../privacy.ts");
28
- applyPrivacy = mod.applyPrivacy;
29
- reloadPrivacyRules = mod.reloadPrivacyRules;
30
- getPrivacyRules = mod.getPrivacyRules;
31
- });
32
-
33
- afterEach(async () => {
34
- try {
35
- await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
36
- } catch {}
37
- });
38
-
39
- afterAll(async () => {
40
- try {
41
- await rm(TEST_DATA_DIR, { recursive: true, force: true });
42
- } catch {}
43
- });
44
-
45
- describe("applyPrivacy", () => {
46
- it("无 privacy.json 时返回原文", () => {
47
- reloadPrivacyRules();
48
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
- });
50
-
51
- it("privacy.json 存在时按规则替换", async () => {
52
- await writeFile(
53
- join(TEST_DATA_DIR, "privacy.json"),
54
- JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
55
- "utf-8",
56
- );
57
- reloadPrivacyRules();
58
-
59
- expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
60
- expect(applyPrivacy("my secret is safe")).toBe("my *** is safe");
61
- expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
62
- });
63
-
64
- it("多规则替换多次出现", async () => {
65
- await writeFile(
66
- join(TEST_DATA_DIR, "privacy.json"),
67
- JSON.stringify({ a: "A", b: "B" }),
68
- "utf-8",
69
- );
70
- reloadPrivacyRules();
71
-
72
- expect(applyPrivacy("a b a b")).toBe("A B A B");
73
- });
74
-
75
- it("空文本直接返回", async () => {
76
- await writeFile(
77
- join(TEST_DATA_DIR, "privacy.json"),
78
- JSON.stringify({ x: "y" }),
79
- "utf-8",
80
- );
81
- reloadPrivacyRules();
82
-
83
- expect(applyPrivacy("")).toBe("");
84
- });
85
-
86
- it("规则中的特殊字符不会被当作正则", async () => {
87
- await writeFile(
88
- join(TEST_DATA_DIR, "privacy.json"),
89
- JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
90
- "utf-8",
91
- );
92
- reloadPrivacyRules();
93
-
94
- expect(applyPrivacy("hello a.b world")).toBe("hello X world");
95
- expect(applyPrivacy("text (test) here")).toBe("text Y here");
96
- expect(applyPrivacy("a *star shines")).toBe("a Z shines");
97
- });
98
-
99
- it("reloadPrivacyRules 强制重新加载", async () => {
100
- await writeFile(
101
- join(TEST_DATA_DIR, "privacy.json"),
102
- JSON.stringify({ old: "OLD" }),
103
- "utf-8",
104
- );
105
- reloadPrivacyRules();
106
-
107
- expect(applyPrivacy("old")).toBe("OLD");
108
- expect(getPrivacyRules()).toEqual({ old: "OLD" });
109
-
110
- // 变更磁盘内容后 reload
111
- await writeFile(
112
- join(TEST_DATA_DIR, "privacy.json"),
113
- JSON.stringify({ new: "NEW" }),
114
- "utf-8",
115
- );
116
- reloadPrivacyRules();
117
-
118
- expect(applyPrivacy("new")).toBe("NEW");
119
- expect(getPrivacyRules()).toEqual({ new: "NEW" });
120
- });
121
-
122
- it("格式错误的 JSON 不抛异常,返回原文", async () => {
123
- await writeFile(
124
- join(TEST_DATA_DIR, "privacy.json"),
125
- "not json",
126
- "utf-8",
127
- );
128
- reloadPrivacyRules();
129
-
130
- expect(applyPrivacy("hello")).toBe("hello");
131
- });
132
-
133
- it("数组格式的 JSON 不抛异常,返回原文", async () => {
134
- await writeFile(
135
- join(TEST_DATA_DIR, "privacy.json"),
136
- JSON.stringify(["a", "b"]),
137
- "utf-8",
138
- );
139
- reloadPrivacyRules();
140
-
141
- expect(applyPrivacy("hello")).toBe("hello");
142
- });
1
+ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ // 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
7
+ const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
8
+ vi.mock("../config.ts", async () => {
9
+ const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
10
+ return {
11
+ ...actual,
12
+ USER_DATA_DIR: TEST_DATA_DIR,
13
+ ts: () => "test-ts",
14
+ };
15
+ });
16
+
17
+ let applyPrivacy: (text: string) => string;
18
+ let reloadPrivacyRules: () => void;
19
+ let getPrivacyRules: () => Record<string, string>;
20
+
21
+ beforeEach(async () => {
22
+ vi.resetModules();
23
+ // 清理临时目录中的 privacy.json
24
+ try {
25
+ await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
26
+ } catch {}
27
+ const mod = await import("../privacy.ts");
28
+ applyPrivacy = mod.applyPrivacy;
29
+ reloadPrivacyRules = mod.reloadPrivacyRules;
30
+ getPrivacyRules = mod.getPrivacyRules;
31
+ });
32
+
33
+ afterEach(async () => {
34
+ try {
35
+ await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
36
+ } catch {}
37
+ });
38
+
39
+ afterAll(async () => {
40
+ try {
41
+ await rm(TEST_DATA_DIR, { recursive: true, force: true });
42
+ } catch {}
43
+ });
44
+
45
+ describe("applyPrivacy", () => {
46
+ it("无 privacy.json 时返回原文", () => {
47
+ reloadPrivacyRules();
48
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
+ });
50
+
51
+ it("privacy.json 存在时按规则替换", async () => {
52
+ await writeFile(
53
+ join(TEST_DATA_DIR, "privacy.json"),
54
+ JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
55
+ "utf-8",
56
+ );
57
+ reloadPrivacyRules();
58
+
59
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
60
+ expect(applyPrivacy("my secret is safe")).toBe("my *** is safe");
61
+ expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
62
+ });
63
+
64
+ it("多规则替换多次出现", async () => {
65
+ await writeFile(
66
+ join(TEST_DATA_DIR, "privacy.json"),
67
+ JSON.stringify({ a: "A", b: "B" }),
68
+ "utf-8",
69
+ );
70
+ reloadPrivacyRules();
71
+
72
+ expect(applyPrivacy("a b a b")).toBe("A B A B");
73
+ });
74
+
75
+ it("空文本直接返回", async () => {
76
+ await writeFile(
77
+ join(TEST_DATA_DIR, "privacy.json"),
78
+ JSON.stringify({ x: "y" }),
79
+ "utf-8",
80
+ );
81
+ reloadPrivacyRules();
82
+
83
+ expect(applyPrivacy("")).toBe("");
84
+ });
85
+
86
+ it("规则中的特殊字符不会被当作正则", async () => {
87
+ await writeFile(
88
+ join(TEST_DATA_DIR, "privacy.json"),
89
+ JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
90
+ "utf-8",
91
+ );
92
+ reloadPrivacyRules();
93
+
94
+ expect(applyPrivacy("hello a.b world")).toBe("hello X world");
95
+ expect(applyPrivacy("text (test) here")).toBe("text Y here");
96
+ expect(applyPrivacy("a *star shines")).toBe("a Z shines");
97
+ });
98
+
99
+ it("reloadPrivacyRules 强制重新加载", async () => {
100
+ await writeFile(
101
+ join(TEST_DATA_DIR, "privacy.json"),
102
+ JSON.stringify({ old: "OLD" }),
103
+ "utf-8",
104
+ );
105
+ reloadPrivacyRules();
106
+
107
+ expect(applyPrivacy("old")).toBe("OLD");
108
+ expect(getPrivacyRules()).toEqual({ old: "OLD" });
109
+
110
+ // 变更磁盘内容后 reload
111
+ await writeFile(
112
+ join(TEST_DATA_DIR, "privacy.json"),
113
+ JSON.stringify({ new: "NEW" }),
114
+ "utf-8",
115
+ );
116
+ reloadPrivacyRules();
117
+
118
+ expect(applyPrivacy("new")).toBe("NEW");
119
+ expect(getPrivacyRules()).toEqual({ new: "NEW" });
120
+ });
121
+
122
+ it("格式错误的 JSON 不抛异常,返回原文", async () => {
123
+ await writeFile(
124
+ join(TEST_DATA_DIR, "privacy.json"),
125
+ "not json",
126
+ "utf-8",
127
+ );
128
+ reloadPrivacyRules();
129
+
130
+ expect(applyPrivacy("hello")).toBe("hello");
131
+ });
132
+
133
+ it("数组格式的 JSON 不抛异常,返回原文", async () => {
134
+ await writeFile(
135
+ join(TEST_DATA_DIR, "privacy.json"),
136
+ JSON.stringify(["a", "b"]),
137
+ "utf-8",
138
+ );
139
+ reloadPrivacyRules();
140
+
141
+ expect(applyPrivacy("hello")).toBe("hello");
142
+ });
143
143
  });