chatccc 0.2.56 → 0.2.57

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 CHANGED
@@ -158,9 +158,9 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
158
158
  "claude": {
159
159
  "enabled": false,
160
160
  "defaultAgent": true,
161
- "model": "claude-sonnet-4-6",
162
- "subagentModel": "",
163
- "effort": "",
161
+ "model": "claude-sonnet-4-6",
162
+ "subagentModel": "",
163
+ "effort": "",
164
164
  "apiKey": "",
165
165
  "baseUrl": ""
166
166
  },
@@ -189,9 +189,9 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
189
189
  | `gitTimeoutSeconds` | `/git` 命令超时时间,默认 180 秒 |
190
190
  | `*.enabled` | 是否启用对应 AI Agent |
191
191
  | `*.defaultAgent` | `/new` 未指定 Agent 时使用哪个工具 |
192
- | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
193
- | `claude.model` / `claude.subagentModel` | Claude Code 主模型 / subagent 模型;`subagentModel` 仅在第三方 API 模式下注入 `CLAUDE_CODE_SUBAGENT_MODEL` |
194
- | `claude.apiKey` / `claude.baseUrl` | 第三方 Anthropic 兼容网关配置;官方 Claude 用户留空 |
192
+ | `cursor.path` / `codex.path` | CLI 可执行文件路径;留空时自动探测或使用 PATH |
193
+ | `claude.model` / `claude.subagentModel` | Claude Code 主模型 / subagent 模型;`subagentModel` 仅在第三方 API 模式下注入 `CLAUDE_CODE_SUBAGENT_MODEL` |
194
+ | `claude.apiKey` / `claude.baseUrl` | 第三方 Anthropic 兼容网关配置;官方 Claude 用户留空 |
195
195
 
196
196
  > 当前 ChatCCC 以 `bypassPermissions` 模式运行,会跳过 Agent 操作确认。请只在可信环境中使用。
197
197
 
@@ -10,14 +10,14 @@
10
10
  "port": 18080,
11
11
  "gitTimeoutSeconds": 180,
12
12
  "allowInterrupt": false,
13
- "claude": {
14
- "enabled": false,
15
- "defaultAgent": true,
16
- "model": "claude-sonnet-4-6",
17
- "subagentModel": "",
18
- "effort": "",
19
- "apiKey": "",
20
- "baseUrl": ""
13
+ "claude": {
14
+ "enabled": false,
15
+ "defaultAgent": true,
16
+ "model": "claude-sonnet-4-6",
17
+ "subagentModel": "",
18
+ "effort": "",
19
+ "apiKey": "",
20
+ "baseUrl": ""
21
21
  },
22
22
  "cursor": {
23
23
  "enabled": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.56",
3
+ "version": "0.2.57",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -771,10 +771,10 @@ describe("createClaudeAdapter — env 注入", () => {
771
771
  expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://api.deepseek.com/anthropic");
772
772
  });
773
773
 
774
- it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
775
- setupMockCreateSession();
776
- const adapter = createClaudeAdapter({
777
- model: "",
774
+ it("apiKey + baseUrl 都设置 → 同时覆盖", async () => {
775
+ setupMockCreateSession();
776
+ const adapter = createClaudeAdapter({
777
+ model: "",
778
778
  effort: "",
779
779
  isEmpty: (v) => v.trim() === "",
780
780
  apiKey: "sk-x",
@@ -784,50 +784,50 @@ describe("createClaudeAdapter — env 注入", () => {
784
784
  await adapter.createSession("/cwd");
785
785
 
786
786
  const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
787
- expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
788
- expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
789
- });
790
-
791
- it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
792
- setupMockCreateSession();
793
- process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
794
- const adapter = createClaudeAdapter({
795
- model: "claude-sonnet-4-6",
796
- subagentModel: "claude-haiku-4-5-20251001",
797
- effort: "",
798
- isEmpty: (v) => v.trim() === "",
799
- apiKey: "",
800
- baseUrl: "",
801
- });
802
-
803
- await adapter.createSession("/cwd");
804
-
805
- const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
806
- expect(opts).not.toHaveProperty("env");
807
- });
808
-
809
- it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
810
- setupMockCreateSession();
811
- process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
812
- const adapter = createClaudeAdapter({
813
- model: "claude-sonnet-4-6",
814
- subagentModel: "claude-haiku-4-5-20251001",
815
- effort: "",
816
- isEmpty: (v) => v.trim() === "",
817
- apiKey: "sk-thirdparty",
818
- baseUrl: "https://gateway.example/anthropic",
819
- });
820
-
821
- await adapter.createSession("/cwd");
822
-
823
- const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
824
- expect(opts.model).toBe("claude-sonnet-4-6");
825
- expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
826
- expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
827
- expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
828
- });
829
-
830
- it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
787
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-x");
788
+ expect(opts.env.ANTHROPIC_BASE_URL).toBe("https://gateway.example/anthropic");
789
+ });
790
+
791
+ it("官方 API 模式下 subagentModel 不注入 env,也不覆盖 Claude 登录环境", async () => {
792
+ setupMockCreateSession();
793
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
794
+ const adapter = createClaudeAdapter({
795
+ model: "claude-sonnet-4-6",
796
+ subagentModel: "claude-haiku-4-5-20251001",
797
+ effort: "",
798
+ isEmpty: (v) => v.trim() === "",
799
+ apiKey: "",
800
+ baseUrl: "",
801
+ });
802
+
803
+ await adapter.createSession("/cwd");
804
+
805
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
806
+ expect(opts).not.toHaveProperty("env");
807
+ });
808
+
809
+ it("第三方 API 模式下 subagentModel 通过 CLAUDE_CODE_SUBAGENT_MODEL 覆盖 Haiku", async () => {
810
+ setupMockCreateSession();
811
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-login";
812
+ const adapter = createClaudeAdapter({
813
+ model: "claude-sonnet-4-6",
814
+ subagentModel: "claude-haiku-4-5-20251001",
815
+ effort: "",
816
+ isEmpty: (v) => v.trim() === "",
817
+ apiKey: "sk-thirdparty",
818
+ baseUrl: "https://gateway.example/anthropic",
819
+ });
820
+
821
+ await adapter.createSession("/cwd");
822
+
823
+ const opts = sdk.unstable_v2_createSession.mock.calls[0][0];
824
+ expect(opts.model).toBe("claude-sonnet-4-6");
825
+ expect(opts.env.ANTHROPIC_API_KEY).toBe("sk-thirdparty");
826
+ expect(opts.env.CLAUDE_CODE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
827
+ expect(opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
828
+ });
829
+
830
+ it("ChatCCC API config is isolated from Claude settings auth/model env", async () => {
831
831
  setupMockCreateSession();
832
832
  process.env.ANTHROPIC_AUTH_TOKEN = "token-from-claude-settings";
833
833
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-from-claude-settings";
@@ -10,9 +10,9 @@ import {
10
10
  CHATCCC_PORT,
11
11
  CLAUDE_API_KEY,
12
12
  CLAUDE_BASE_URL,
13
- CLAUDE_EFFORT,
14
- CLAUDE_MODEL,
15
- CLAUDE_SUBAGENT_MODEL,
13
+ CLAUDE_EFFORT,
14
+ CLAUDE_MODEL,
15
+ CLAUDE_SUBAGENT_MODEL,
16
16
  CURSOR_AGENT_ARGS,
17
17
  CURSOR_AGENT_COMMAND,
18
18
  FEISHU_ENABLED,
@@ -44,10 +44,10 @@ const baseAppConfig: AppConfig = {
44
44
  allowInterrupt: false,
45
45
  claude: {
46
46
  enabled: true,
47
- defaultAgent: true,
48
- model: "initial-model",
49
- subagentModel: "initial-subagent-model",
50
- effort: "initial-effort",
47
+ defaultAgent: true,
48
+ model: "initial-model",
49
+ subagentModel: "initial-subagent-model",
50
+ effort: "initial-effort",
51
51
  apiKey: "sk-initial",
52
52
  baseUrl: "https://initial.gw/anthropic",
53
53
  },
@@ -80,22 +80,22 @@ describe("applyLoadedConfig — 刷新 export let 常量", () => {
80
80
  expect(APP_SECRET).toBe("NEW_APP_SECRET");
81
81
  });
82
82
 
83
- it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
83
+ it("更新 Claude 配置(model / subagentModel / effort / apiKey / baseUrl)", () => {
84
84
  applyLoadedConfig({
85
85
  ...structuredClone(baseAppConfig),
86
86
  claude: {
87
87
  enabled: true,
88
- defaultAgent: true,
89
- model: "claude-sonnet-4-6",
90
- subagentModel: "claude-haiku-4-5-20251001",
91
- effort: "high",
88
+ defaultAgent: true,
89
+ model: "claude-sonnet-4-6",
90
+ subagentModel: "claude-haiku-4-5-20251001",
91
+ effort: "high",
92
92
  apiKey: "sk-newkey",
93
93
  baseUrl: "https://gw2.example/anthropic",
94
94
  },
95
95
  });
96
96
 
97
- expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
98
- expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
97
+ expect(CLAUDE_MODEL).toBe("claude-sonnet-4-6");
98
+ expect(CLAUDE_SUBAGENT_MODEL).toBe("claude-haiku-4-5-20251001");
99
99
  expect(CLAUDE_EFFORT).toBe("high");
100
100
  expect(CLAUDE_API_KEY).toBe("sk-newkey");
101
101
  expect(CLAUDE_BASE_URL).toBe("https://gw2.example/anthropic");
@@ -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("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
+ });
@@ -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
  });
@@ -662,7 +662,7 @@ describe("accumulateBlockContent", () => {
662
662
  it("accumulates thinking block into accumulatedContent", () => {
663
663
  const s = freshState();
664
664
  accumulateBlockContent({ type: "thinking", thinking: "Let me think..." }, s);
665
- expect(s.accumulatedContent).toBe("Let me think...");
665
+ expect(s.accumulatedContent).toBe("\n> Let me think...\n");
666
666
  expect(s.chunkCount).toBe(1);
667
667
  expect(s.finalText).toBe("");
668
668
  });
@@ -786,7 +786,7 @@ describe("accumulateBlockContent", () => {
786
786
  );
787
787
  accumulateBlockContent({ type: "text", text: "I found the results." }, s);
788
788
 
789
- expect(s.accumulatedContent).toContain("Hmm...");
789
+ expect(s.accumulatedContent).toContain("> Hmm...");
790
790
  expect(s.accumulatedContent).toContain("Grep");
791
791
  expect(s.accumulatedContent).toContain("found 3 matches");
792
792
  expect(s.finalText).toBe("I found the results.");