chatccc 0.2.176 → 0.2.177

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,8 +6,8 @@ import { join } from "node:path";
6
6
  import type { PlatformAdapter } from "../platform-adapter.ts";
7
7
  import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
8
8
 
9
- const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
- const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
9
+ const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
+ const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
11
11
 
12
12
  vi.mock("../im-skills.ts", () => ({
13
13
  buildImSkillsPrompt: async () => "",
@@ -15,7 +15,7 @@ vi.mock("../im-skills.ts", () => ({
15
15
  exportSkillSubDocs: async () => {},
16
16
  }));
17
17
 
18
- vi.mock("../stream-state.ts", () => ({
18
+ vi.mock("../stream-state.ts", () => ({
19
19
  readStreamState: async (sessionId: string) => {
20
20
  const state = mockStreamStates.get(sessionId);
21
21
  if (!state) return null;
@@ -50,14 +50,14 @@ vi.mock("../stream-state.ts", () => ({
50
50
  cwd,
51
51
  tool,
52
52
  }),
53
- fixStaleStreamStates: async () => {},
54
- }));
55
-
56
- vi.mock("../feishu-platform.ts", () => ({
57
- getCodexUsageSummary: mockGetCodexUsageSummary,
58
- getTenantAccessToken: vi.fn(async () => "tenant-token"),
59
- sendPostMessage: vi.fn(async () => true),
60
- }));
53
+ fixStaleStreamStates: async () => {},
54
+ }));
55
+
56
+ vi.mock("../feishu-platform.ts", () => ({
57
+ getCodexUsageSummary: mockGetCodexUsageSummary,
58
+ getTenantAccessToken: vi.fn(async () => "tenant-token"),
59
+ sendPostMessage: vi.fn(async () => true),
60
+ }));
61
61
 
62
62
  import { handleCommand } from "../orchestrator.ts";
63
63
  import {
@@ -120,14 +120,14 @@ describe("handleCommand WeChat processing ack", () => {
120
120
  _setSessionToolsFileForTest(join(tempDir, "sessions.json"));
121
121
  resetState();
122
122
  resetBindingState();
123
- mockStreamStates.clear();
124
- mockGetCodexUsageSummary.mockReset();
125
- mockGetCodexUsageSummary.mockResolvedValue({
126
- fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
127
- weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
128
- });
129
- _setAdapterForToolForTest("claude", mockAdapter());
130
- });
123
+ mockStreamStates.clear();
124
+ mockGetCodexUsageSummary.mockReset();
125
+ mockGetCodexUsageSummary.mockResolvedValue({
126
+ fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
127
+ weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
128
+ });
129
+ _setAdapterForToolForTest("claude", mockAdapter());
130
+ });
131
131
 
132
132
  afterEach(async () => {
133
133
  resetState();
@@ -249,7 +249,7 @@ describe("handleCommand WeChat processing ack", () => {
249
249
  expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
250
250
  });
251
251
 
252
- it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
252
+ it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
253
253
  const platform = mockPlatform("feishu");
254
254
  await recordSessionRegistry({
255
255
  chatId: "feishu-p2p",
@@ -264,76 +264,76 @@ describe("handleCommand WeChat processing ack", () => {
264
264
  expect(platform.createGroup).not.toHaveBeenCalled();
265
265
  expect(platform.sendRawCard).toHaveBeenCalled();
266
266
  const registry = await loadSessionRegistryForBinding();
267
- expect(registry["feishu-p2p"]).toBeUndefined();
268
- });
269
-
270
- it("handles /usage without creating a new Feishu group", async () => {
271
- const platform = mockPlatform("feishu");
272
- mockGetCodexUsageSummary.mockResolvedValue({
273
- fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
274
- weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
275
- });
276
-
277
- await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
278
-
279
- expect(platform.createGroup).not.toHaveBeenCalled();
280
- expect(platform.sendCard).toHaveBeenCalledWith(
281
- "feishu-p2p",
282
- "Codex Usage",
283
- expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
284
- "blue",
285
- );
286
- expect(platform.sendCard).toHaveBeenCalledWith(
287
- "feishu-p2p",
288
- "Codex Usage",
289
- expect.stringContaining("约 2小时52分钟后"),
290
- "blue",
291
- );
292
- expect(platform.sendCard).toHaveBeenCalledWith(
293
- "feishu-p2p",
294
- "Codex Usage",
295
- expect.stringContaining("[███████░░░░░░░░░░░░░]"),
296
- "blue",
297
- );
298
- expect(platform.sendCard).toHaveBeenCalledWith(
299
- "feishu-p2p",
300
- "Codex Usage",
301
- expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
302
- "blue",
303
- );
304
- expect(platform.sendCard).toHaveBeenCalledWith(
305
- "feishu-p2p",
306
- "Codex Usage",
307
- expect.stringContaining("约 3天18小时17分钟后"),
308
- "blue",
309
- );
310
- expect(platform.sendCard).toHaveBeenCalledWith(
311
- "feishu-p2p",
312
- "Codex Usage",
313
- expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
314
- "blue",
315
- );
316
- });
317
-
318
- it("only advertises /usage in new Codex session ready messages", async () => {
319
- const codexPlatform = mockPlatform("feishu");
320
- _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
321
-
322
- await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
323
-
324
- expect(codexPlatform.sendCard).toHaveBeenCalledWith(
325
- "feishu-group",
326
- "Codex Session Ready",
327
- expect.stringContaining("发送 **/usage** 查看 Codex 5h 和周用量。"),
328
- "green",
329
- );
330
-
331
- const claudePlatform = mockPlatform("feishu");
332
- await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
333
-
334
- const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
335
- ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
336
- );
337
- expect(claudeReadyCall?.[2]).not.toContain("/usage");
338
- });
339
- });
267
+ expect(registry["feishu-p2p"]).toBeUndefined();
268
+ });
269
+
270
+ it("handles /usage without creating a new Feishu group", async () => {
271
+ const platform = mockPlatform("feishu");
272
+ mockGetCodexUsageSummary.mockResolvedValue({
273
+ fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
274
+ weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
275
+ });
276
+
277
+ await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
278
+
279
+ expect(platform.createGroup).not.toHaveBeenCalled();
280
+ expect(platform.sendCard).toHaveBeenCalledWith(
281
+ "feishu-p2p",
282
+ "Codex Usage",
283
+ expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
284
+ "blue",
285
+ );
286
+ expect(platform.sendCard).toHaveBeenCalledWith(
287
+ "feishu-p2p",
288
+ "Codex Usage",
289
+ expect.stringContaining("约 2小时52分钟后"),
290
+ "blue",
291
+ );
292
+ expect(platform.sendCard).toHaveBeenCalledWith(
293
+ "feishu-p2p",
294
+ "Codex Usage",
295
+ expect.stringContaining("[███████░░░░░░░░░░░░░]"),
296
+ "blue",
297
+ );
298
+ expect(platform.sendCard).toHaveBeenCalledWith(
299
+ "feishu-p2p",
300
+ "Codex Usage",
301
+ expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
302
+ "blue",
303
+ );
304
+ expect(platform.sendCard).toHaveBeenCalledWith(
305
+ "feishu-p2p",
306
+ "Codex Usage",
307
+ expect.stringContaining("约 3天18小时17分钟后"),
308
+ "blue",
309
+ );
310
+ expect(platform.sendCard).toHaveBeenCalledWith(
311
+ "feishu-p2p",
312
+ "Codex Usage",
313
+ expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
314
+ "blue",
315
+ );
316
+ });
317
+
318
+ it("only advertises /usage in new Codex session ready messages", async () => {
319
+ const codexPlatform = mockPlatform("feishu");
320
+ _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
321
+
322
+ await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
323
+
324
+ expect(codexPlatform.sendCard).toHaveBeenCalledWith(
325
+ "feishu-group",
326
+ "Codex Session Ready",
327
+ expect.stringContaining("发送 **/usage** 查看 Codex 5h 和周用量。"),
328
+ "green",
329
+ );
330
+
331
+ const claudePlatform = mockPlatform("feishu");
332
+ await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
333
+
334
+ const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
335
+ ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
336
+ );
337
+ expect(claudeReadyCall?.[2]).not.toContain("/usage");
338
+ });
339
+ });
@@ -1,198 +1,198 @@
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
- const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
7
- vi.mock("../config.ts", async () => {
8
- const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
9
- return {
10
- ...actual,
11
- USER_DATA_DIR: TEST_DATA_DIR,
12
- ts: () => "test-ts",
13
- };
14
- });
15
-
16
- let applyPrivacy: (text: string) => string;
17
- let reloadPrivacyRules: () => void;
18
- let getPrivacyRules: () => Record<string, string>;
19
- let getPrivacyConfig: () => { enabled: boolean; rules: Record<string, string> };
20
-
21
- beforeEach(async () => {
22
- vi.resetModules();
23
- try {
24
- await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
25
- } catch {}
26
- const mod = await import("../privacy.ts");
27
- applyPrivacy = mod.applyPrivacy;
28
- reloadPrivacyRules = mod.reloadPrivacyRules;
29
- getPrivacyRules = mod.getPrivacyRules;
30
- getPrivacyConfig = mod.getPrivacyConfig;
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("returns original text when privacy.json is missing", () => {
47
- reloadPrivacyRules();
48
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
- });
50
-
51
- it("supports legacy flat privacy rules", 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("supports privacy.json schema with enabled=false", async () => {
65
- await writeFile(
66
- join(TEST_DATA_DIR, "privacy.json"),
67
- JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } }),
68
- "utf-8",
69
- );
70
- reloadPrivacyRules();
71
-
72
- expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
73
- expect(getPrivacyRules()).toEqual({ weizhangjian: "wzj" });
74
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
75
- });
76
-
77
- it("supports privacy.json schema with enabled=true", async () => {
78
- await writeFile(
79
- join(TEST_DATA_DIR, "privacy.json"),
80
- JSON.stringify({ enabled: true, rules: { weizhangjian: "wzj" } }),
81
- "utf-8",
82
- );
83
- reloadPrivacyRules();
84
-
85
- expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
86
- });
87
-
88
- it("accepts UTF-8 BOM in privacy.json", async () => {
89
- await writeFile(
90
- join(TEST_DATA_DIR, "privacy.json"),
91
- `\uFEFF${JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } })}`,
92
- "utf-8",
93
- );
94
- reloadPrivacyRules();
95
-
96
- expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
97
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
98
- });
99
-
100
- it("auto reloads privacy.json changes without explicit reload", async () => {
101
- await writeFile(
102
- join(TEST_DATA_DIR, "privacy.json"),
103
- JSON.stringify({ weizhangjian: "wzj" }),
104
- "utf-8",
105
- );
106
- reloadPrivacyRules();
107
-
108
- expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
109
-
110
- await writeFile(
111
- join(TEST_DATA_DIR, "privacy.json"),
112
- JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj-disabled" } }),
113
- "utf-8",
114
- );
115
-
116
- expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
117
- expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj-disabled" } });
118
- });
119
-
120
- it("replaces multiple rules and repeated occurrences", async () => {
121
- await writeFile(
122
- join(TEST_DATA_DIR, "privacy.json"),
123
- JSON.stringify({ a: "A", b: "B" }),
124
- "utf-8",
125
- );
126
- reloadPrivacyRules();
127
-
128
- expect(applyPrivacy("a b a b")).toBe("A B A B");
129
- });
130
-
131
- it("returns empty text directly", async () => {
132
- await writeFile(
133
- join(TEST_DATA_DIR, "privacy.json"),
134
- JSON.stringify({ x: "y" }),
135
- "utf-8",
136
- );
137
- reloadPrivacyRules();
138
-
139
- expect(applyPrivacy("")).toBe("");
140
- });
141
-
142
- it("treats special characters in rule keys literally", async () => {
143
- await writeFile(
144
- join(TEST_DATA_DIR, "privacy.json"),
145
- JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
146
- "utf-8",
147
- );
148
- reloadPrivacyRules();
149
-
150
- expect(applyPrivacy("hello a.b world")).toBe("hello X world");
151
- expect(applyPrivacy("text (test) here")).toBe("text Y here");
152
- expect(applyPrivacy("a *star shines")).toBe("a Z shines");
153
- });
154
-
155
- it("reloadPrivacyRules forces a reload", async () => {
156
- await writeFile(
157
- join(TEST_DATA_DIR, "privacy.json"),
158
- JSON.stringify({ old: "OLD" }),
159
- "utf-8",
160
- );
161
- reloadPrivacyRules();
162
-
163
- expect(applyPrivacy("old")).toBe("OLD");
164
- expect(getPrivacyRules()).toEqual({ old: "OLD" });
165
-
166
- await writeFile(
167
- join(TEST_DATA_DIR, "privacy.json"),
168
- JSON.stringify({ new: "NEW" }),
169
- "utf-8",
170
- );
171
- reloadPrivacyRules();
172
-
173
- expect(applyPrivacy("new")).toBe("NEW");
174
- expect(getPrivacyRules()).toEqual({ new: "NEW" });
175
- });
176
-
177
- it("returns original text for malformed JSON", async () => {
178
- await writeFile(
179
- join(TEST_DATA_DIR, "privacy.json"),
180
- "not json",
181
- "utf-8",
182
- );
183
- reloadPrivacyRules();
184
-
185
- expect(applyPrivacy("hello")).toBe("hello");
186
- });
187
-
188
- it("returns original text for array JSON", async () => {
189
- await writeFile(
190
- join(TEST_DATA_DIR, "privacy.json"),
191
- JSON.stringify(["a", "b"]),
192
- "utf-8",
193
- );
194
- reloadPrivacyRules();
195
-
196
- expect(applyPrivacy("hello")).toBe("hello");
197
- });
198
- });
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
+ const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
7
+ vi.mock("../config.ts", async () => {
8
+ const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
9
+ return {
10
+ ...actual,
11
+ USER_DATA_DIR: TEST_DATA_DIR,
12
+ ts: () => "test-ts",
13
+ };
14
+ });
15
+
16
+ let applyPrivacy: (text: string) => string;
17
+ let reloadPrivacyRules: () => void;
18
+ let getPrivacyRules: () => Record<string, string>;
19
+ let getPrivacyConfig: () => { enabled: boolean; rules: Record<string, string> };
20
+
21
+ beforeEach(async () => {
22
+ vi.resetModules();
23
+ try {
24
+ await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
25
+ } catch {}
26
+ const mod = await import("../privacy.ts");
27
+ applyPrivacy = mod.applyPrivacy;
28
+ reloadPrivacyRules = mod.reloadPrivacyRules;
29
+ getPrivacyRules = mod.getPrivacyRules;
30
+ getPrivacyConfig = mod.getPrivacyConfig;
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("returns original text when privacy.json is missing", () => {
47
+ reloadPrivacyRules();
48
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
49
+ });
50
+
51
+ it("supports legacy flat privacy rules", 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("supports privacy.json schema with enabled=false", async () => {
65
+ await writeFile(
66
+ join(TEST_DATA_DIR, "privacy.json"),
67
+ JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } }),
68
+ "utf-8",
69
+ );
70
+ reloadPrivacyRules();
71
+
72
+ expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
73
+ expect(getPrivacyRules()).toEqual({ weizhangjian: "wzj" });
74
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
75
+ });
76
+
77
+ it("supports privacy.json schema with enabled=true", async () => {
78
+ await writeFile(
79
+ join(TEST_DATA_DIR, "privacy.json"),
80
+ JSON.stringify({ enabled: true, rules: { weizhangjian: "wzj" } }),
81
+ "utf-8",
82
+ );
83
+ reloadPrivacyRules();
84
+
85
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
86
+ });
87
+
88
+ it("accepts UTF-8 BOM in privacy.json", async () => {
89
+ await writeFile(
90
+ join(TEST_DATA_DIR, "privacy.json"),
91
+ `\uFEFF${JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj" } })}`,
92
+ "utf-8",
93
+ );
94
+ reloadPrivacyRules();
95
+
96
+ expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj" } });
97
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
98
+ });
99
+
100
+ it("auto reloads privacy.json changes without explicit reload", async () => {
101
+ await writeFile(
102
+ join(TEST_DATA_DIR, "privacy.json"),
103
+ JSON.stringify({ weizhangjian: "wzj" }),
104
+ "utf-8",
105
+ );
106
+ reloadPrivacyRules();
107
+
108
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello wzj");
109
+
110
+ await writeFile(
111
+ join(TEST_DATA_DIR, "privacy.json"),
112
+ JSON.stringify({ enabled: false, rules: { weizhangjian: "wzj-disabled" } }),
113
+ "utf-8",
114
+ );
115
+
116
+ expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
117
+ expect(getPrivacyConfig()).toEqual({ enabled: false, rules: { weizhangjian: "wzj-disabled" } });
118
+ });
119
+
120
+ it("replaces multiple rules and repeated occurrences", async () => {
121
+ await writeFile(
122
+ join(TEST_DATA_DIR, "privacy.json"),
123
+ JSON.stringify({ a: "A", b: "B" }),
124
+ "utf-8",
125
+ );
126
+ reloadPrivacyRules();
127
+
128
+ expect(applyPrivacy("a b a b")).toBe("A B A B");
129
+ });
130
+
131
+ it("returns empty text directly", async () => {
132
+ await writeFile(
133
+ join(TEST_DATA_DIR, "privacy.json"),
134
+ JSON.stringify({ x: "y" }),
135
+ "utf-8",
136
+ );
137
+ reloadPrivacyRules();
138
+
139
+ expect(applyPrivacy("")).toBe("");
140
+ });
141
+
142
+ it("treats special characters in rule keys literally", async () => {
143
+ await writeFile(
144
+ join(TEST_DATA_DIR, "privacy.json"),
145
+ JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
146
+ "utf-8",
147
+ );
148
+ reloadPrivacyRules();
149
+
150
+ expect(applyPrivacy("hello a.b world")).toBe("hello X world");
151
+ expect(applyPrivacy("text (test) here")).toBe("text Y here");
152
+ expect(applyPrivacy("a *star shines")).toBe("a Z shines");
153
+ });
154
+
155
+ it("reloadPrivacyRules forces a reload", async () => {
156
+ await writeFile(
157
+ join(TEST_DATA_DIR, "privacy.json"),
158
+ JSON.stringify({ old: "OLD" }),
159
+ "utf-8",
160
+ );
161
+ reloadPrivacyRules();
162
+
163
+ expect(applyPrivacy("old")).toBe("OLD");
164
+ expect(getPrivacyRules()).toEqual({ old: "OLD" });
165
+
166
+ await writeFile(
167
+ join(TEST_DATA_DIR, "privacy.json"),
168
+ JSON.stringify({ new: "NEW" }),
169
+ "utf-8",
170
+ );
171
+ reloadPrivacyRules();
172
+
173
+ expect(applyPrivacy("new")).toBe("NEW");
174
+ expect(getPrivacyRules()).toEqual({ new: "NEW" });
175
+ });
176
+
177
+ it("returns original text for malformed JSON", async () => {
178
+ await writeFile(
179
+ join(TEST_DATA_DIR, "privacy.json"),
180
+ "not json",
181
+ "utf-8",
182
+ );
183
+ reloadPrivacyRules();
184
+
185
+ expect(applyPrivacy("hello")).toBe("hello");
186
+ });
187
+
188
+ it("returns original text for array JSON", async () => {
189
+ await writeFile(
190
+ join(TEST_DATA_DIR, "privacy.json"),
191
+ JSON.stringify(["a", "b"]),
192
+ "utf-8",
193
+ );
194
+ reloadPrivacyRules();
195
+
196
+ expect(applyPrivacy("hello")).toBe("hello");
197
+ });
198
+ });