chatccc 0.2.175 → 0.2.176
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 +3 -3
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +2 -2
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/package.json +1 -1
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/claude-adapter.test.ts +592 -592
- package/src/__tests__/config-reload.test.ts +18 -18
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +129 -129
- package/src/__tests__/feishu-platform.test.ts +16 -16
- package/src/__tests__/format-message.test.ts +272 -272
- package/src/__tests__/orchestrator.test.ts +93 -93
- package/src/__tests__/privacy.test.ts +67 -12
- package/src/__tests__/web-ui.test.ts +92 -92
- package/src/adapters/claude-adapter.ts +566 -566
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/agent-stop-stuck.ts +129 -129
- package/src/cards.ts +4 -4
- package/src/feishu-api.ts +236 -236
- package/src/feishu-platform.ts +15 -15
- package/src/format-message.ts +213 -213
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +112 -112
- package/src/privacy.ts +89 -39
- package/src/session-chat-binding.ts +6 -6
- package/src/sim-platform.ts +14 -14
- package/src/web-ui.ts +6 -6
|
@@ -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
|
+
});
|
|
@@ -3,7 +3,6 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
// 在 import privacy 之前 mock config,让 USER_DATA_DIR 指向临时目录
|
|
7
6
|
const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-privacy-test-"));
|
|
8
7
|
vi.mock("../config.ts", async () => {
|
|
9
8
|
const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
|
|
@@ -17,10 +16,10 @@ vi.mock("../config.ts", async () => {
|
|
|
17
16
|
let applyPrivacy: (text: string) => string;
|
|
18
17
|
let reloadPrivacyRules: () => void;
|
|
19
18
|
let getPrivacyRules: () => Record<string, string>;
|
|
19
|
+
let getPrivacyConfig: () => { enabled: boolean; rules: Record<string, string> };
|
|
20
20
|
|
|
21
21
|
beforeEach(async () => {
|
|
22
22
|
vi.resetModules();
|
|
23
|
-
// 清理临时目录中的 privacy.json
|
|
24
23
|
try {
|
|
25
24
|
await rm(join(TEST_DATA_DIR, "privacy.json"), { force: true });
|
|
26
25
|
} catch {}
|
|
@@ -28,6 +27,7 @@ beforeEach(async () => {
|
|
|
28
27
|
applyPrivacy = mod.applyPrivacy;
|
|
29
28
|
reloadPrivacyRules = mod.reloadPrivacyRules;
|
|
30
29
|
getPrivacyRules = mod.getPrivacyRules;
|
|
30
|
+
getPrivacyConfig = mod.getPrivacyConfig;
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
afterEach(async () => {
|
|
@@ -43,12 +43,12 @@ afterAll(async () => {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
describe("applyPrivacy", () => {
|
|
46
|
-
it("
|
|
46
|
+
it("returns original text when privacy.json is missing", () => {
|
|
47
47
|
reloadPrivacyRules();
|
|
48
48
|
expect(applyPrivacy("hello weizhangjian")).toBe("hello weizhangjian");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it("privacy
|
|
51
|
+
it("supports legacy flat privacy rules", async () => {
|
|
52
52
|
await writeFile(
|
|
53
53
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
54
54
|
JSON.stringify({ weizhangjian: "wzj", secret: "***" }),
|
|
@@ -61,7 +61,63 @@ describe("applyPrivacy", () => {
|
|
|
61
61
|
expect(applyPrivacy("weizhangjian and secret")).toBe("wzj and ***");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it("
|
|
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 () => {
|
|
65
121
|
await writeFile(
|
|
66
122
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
67
123
|
JSON.stringify({ a: "A", b: "B" }),
|
|
@@ -72,7 +128,7 @@ describe("applyPrivacy", () => {
|
|
|
72
128
|
expect(applyPrivacy("a b a b")).toBe("A B A B");
|
|
73
129
|
});
|
|
74
130
|
|
|
75
|
-
it("
|
|
131
|
+
it("returns empty text directly", async () => {
|
|
76
132
|
await writeFile(
|
|
77
133
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
78
134
|
JSON.stringify({ x: "y" }),
|
|
@@ -83,7 +139,7 @@ describe("applyPrivacy", () => {
|
|
|
83
139
|
expect(applyPrivacy("")).toBe("");
|
|
84
140
|
});
|
|
85
141
|
|
|
86
|
-
it("
|
|
142
|
+
it("treats special characters in rule keys literally", async () => {
|
|
87
143
|
await writeFile(
|
|
88
144
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
89
145
|
JSON.stringify({ "a.b": "X", "(test)": "Y", "*star": "Z" }),
|
|
@@ -96,7 +152,7 @@ describe("applyPrivacy", () => {
|
|
|
96
152
|
expect(applyPrivacy("a *star shines")).toBe("a Z shines");
|
|
97
153
|
});
|
|
98
154
|
|
|
99
|
-
it("reloadPrivacyRules
|
|
155
|
+
it("reloadPrivacyRules forces a reload", async () => {
|
|
100
156
|
await writeFile(
|
|
101
157
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
102
158
|
JSON.stringify({ old: "OLD" }),
|
|
@@ -107,7 +163,6 @@ describe("applyPrivacy", () => {
|
|
|
107
163
|
expect(applyPrivacy("old")).toBe("OLD");
|
|
108
164
|
expect(getPrivacyRules()).toEqual({ old: "OLD" });
|
|
109
165
|
|
|
110
|
-
// 变更磁盘内容后 reload
|
|
111
166
|
await writeFile(
|
|
112
167
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
113
168
|
JSON.stringify({ new: "NEW" }),
|
|
@@ -119,7 +174,7 @@ describe("applyPrivacy", () => {
|
|
|
119
174
|
expect(getPrivacyRules()).toEqual({ new: "NEW" });
|
|
120
175
|
});
|
|
121
176
|
|
|
122
|
-
it("
|
|
177
|
+
it("returns original text for malformed JSON", async () => {
|
|
123
178
|
await writeFile(
|
|
124
179
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
125
180
|
"not json",
|
|
@@ -130,7 +185,7 @@ describe("applyPrivacy", () => {
|
|
|
130
185
|
expect(applyPrivacy("hello")).toBe("hello");
|
|
131
186
|
});
|
|
132
187
|
|
|
133
|
-
it("
|
|
188
|
+
it("returns original text for array JSON", async () => {
|
|
134
189
|
await writeFile(
|
|
135
190
|
join(TEST_DATA_DIR, "privacy.json"),
|
|
136
191
|
JSON.stringify(["a", "b"]),
|
|
@@ -140,4 +195,4 @@ describe("applyPrivacy", () => {
|
|
|
140
195
|
|
|
141
196
|
expect(applyPrivacy("hello")).toBe("hello");
|
|
142
197
|
});
|
|
143
|
-
});
|
|
198
|
+
});
|
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
PAGE_HTML,
|
|
4
|
-
chooseStartPath,
|
|
5
|
-
unflattenConfig,
|
|
6
|
-
} from "../web-ui.ts";
|
|
7
|
-
|
|
8
|
-
describe("unflattenConfig", () => {
|
|
9
|
-
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
10
|
-
expect(
|
|
11
|
-
unflattenConfig({
|
|
12
|
-
CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
13
|
-
CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
|
|
14
|
-
}),
|
|
15
|
-
).toEqual({
|
|
16
|
-
claude: {
|
|
17
|
-
model: "claude-sonnet-4-6",
|
|
18
|
-
subagentModel: "claude-haiku-4-5-20251001",
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("maps Claude apiKey and baseUrl into claude config", () => {
|
|
24
|
-
expect(
|
|
25
|
-
unflattenConfig({
|
|
26
|
-
CHATCCC_ANTHROPIC_API_KEY: "sk-test-key",
|
|
27
|
-
CHATCCC_ANTHROPIC_BASE_URL: "https://api.example.com",
|
|
28
|
-
}),
|
|
29
|
-
).toEqual({
|
|
30
|
-
claude: {
|
|
31
|
-
apiKey: "sk-test-key",
|
|
32
|
-
baseUrl: "https://api.example.com",
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("dashboard edit modal", () => {
|
|
39
|
-
it("shows the edit modal and overlay when a section edit button is clicked", () => {
|
|
40
|
-
expect(PAGE_HTML).toContain("function editSection(section)");
|
|
41
|
-
expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
|
|
42
|
-
expect(PAGE_HTML).toContain("document.getElementById('edit-overlay').classList.remove('hidden');");
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// chooseStartPath — /api/start 的路径选择
|
|
48
|
-
// 关键护栏:
|
|
49
|
-
// - setup 模式(hasInplaceActivateHook=true)下 isServiceRunning 永远为 true
|
|
50
|
-
// (setup 进程自己占着 PID 文件),必须无条件走 inplace;否则用户点
|
|
51
|
-
// "保存并启动"将永远拿到 "Service is already running"。
|
|
52
|
-
// - dashboard 模式 + service 已运行(通常就是当前进程自己)→ "reload":
|
|
53
|
-
// 用户点"保存并启动"想让新 config 生效,但服务正在跑——不真重启,仅
|
|
54
|
-
// 调用 reloadConfigFromDisk() 刷新进程内 export let 常量。绝不能再
|
|
55
|
-
// 返回"already running"挡用户路。
|
|
56
|
-
// - dashboard 模式 + service 未运行 → spawn 一个新的(旧 service 退出后场景)。
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
describe("chooseStartPath", () => {
|
|
60
|
-
it("setup 模式(注入 inplace hook)→ inplace(不管 PID 文件状态)", () => {
|
|
61
|
-
expect(
|
|
62
|
-
chooseStartPath({
|
|
63
|
-
hasInplaceActivateHook: true,
|
|
64
|
-
isServiceRunning: true,
|
|
65
|
-
}),
|
|
66
|
-
).toBe("inplace");
|
|
67
|
-
expect(
|
|
68
|
-
chooseStartPath({
|
|
69
|
-
hasInplaceActivateHook: true,
|
|
70
|
-
isServiceRunning: false,
|
|
71
|
-
}),
|
|
72
|
-
).toBe("inplace");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("dashboard 模式 + service 已运行 → reload(仅刷新 config,不真重启)", () => {
|
|
76
|
-
expect(
|
|
77
|
-
chooseStartPath({
|
|
78
|
-
hasInplaceActivateHook: false,
|
|
79
|
-
isServiceRunning: true,
|
|
80
|
-
}),
|
|
81
|
-
).toBe("reload");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("dashboard 模式 + service 未运行 → spawn", () => {
|
|
85
|
-
expect(
|
|
86
|
-
chooseStartPath({
|
|
87
|
-
hasInplaceActivateHook: false,
|
|
88
|
-
isServiceRunning: false,
|
|
89
|
-
}),
|
|
90
|
-
).toBe("spawn");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
PAGE_HTML,
|
|
4
|
+
chooseStartPath,
|
|
5
|
+
unflattenConfig,
|
|
6
|
+
} from "../web-ui.ts";
|
|
7
|
+
|
|
8
|
+
describe("unflattenConfig", () => {
|
|
9
|
+
it("maps Claude subagent model into claude.subagentModel", () => {
|
|
10
|
+
expect(
|
|
11
|
+
unflattenConfig({
|
|
12
|
+
CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
|
|
13
|
+
CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
|
|
14
|
+
}),
|
|
15
|
+
).toEqual({
|
|
16
|
+
claude: {
|
|
17
|
+
model: "claude-sonnet-4-6",
|
|
18
|
+
subagentModel: "claude-haiku-4-5-20251001",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("maps Claude apiKey and baseUrl into claude config", () => {
|
|
24
|
+
expect(
|
|
25
|
+
unflattenConfig({
|
|
26
|
+
CHATCCC_ANTHROPIC_API_KEY: "sk-test-key",
|
|
27
|
+
CHATCCC_ANTHROPIC_BASE_URL: "https://api.example.com",
|
|
28
|
+
}),
|
|
29
|
+
).toEqual({
|
|
30
|
+
claude: {
|
|
31
|
+
apiKey: "sk-test-key",
|
|
32
|
+
baseUrl: "https://api.example.com",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("dashboard edit modal", () => {
|
|
39
|
+
it("shows the edit modal and overlay when a section edit button is clicked", () => {
|
|
40
|
+
expect(PAGE_HTML).toContain("function editSection(section)");
|
|
41
|
+
expect(PAGE_HTML).toContain("document.getElementById('edit-modal').classList.remove('hidden');");
|
|
42
|
+
expect(PAGE_HTML).toContain("document.getElementById('edit-overlay').classList.remove('hidden');");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// chooseStartPath — /api/start 的路径选择
|
|
48
|
+
// 关键护栏:
|
|
49
|
+
// - setup 模式(hasInplaceActivateHook=true)下 isServiceRunning 永远为 true
|
|
50
|
+
// (setup 进程自己占着 PID 文件),必须无条件走 inplace;否则用户点
|
|
51
|
+
// "保存并启动"将永远拿到 "Service is already running"。
|
|
52
|
+
// - dashboard 模式 + service 已运行(通常就是当前进程自己)→ "reload":
|
|
53
|
+
// 用户点"保存并启动"想让新 config 生效,但服务正在跑——不真重启,仅
|
|
54
|
+
// 调用 reloadConfigFromDisk() 刷新进程内 export let 常量。绝不能再
|
|
55
|
+
// 返回"already running"挡用户路。
|
|
56
|
+
// - dashboard 模式 + service 未运行 → spawn 一个新的(旧 service 退出后场景)。
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
describe("chooseStartPath", () => {
|
|
60
|
+
it("setup 模式(注入 inplace hook)→ inplace(不管 PID 文件状态)", () => {
|
|
61
|
+
expect(
|
|
62
|
+
chooseStartPath({
|
|
63
|
+
hasInplaceActivateHook: true,
|
|
64
|
+
isServiceRunning: true,
|
|
65
|
+
}),
|
|
66
|
+
).toBe("inplace");
|
|
67
|
+
expect(
|
|
68
|
+
chooseStartPath({
|
|
69
|
+
hasInplaceActivateHook: true,
|
|
70
|
+
isServiceRunning: false,
|
|
71
|
+
}),
|
|
72
|
+
).toBe("inplace");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("dashboard 模式 + service 已运行 → reload(仅刷新 config,不真重启)", () => {
|
|
76
|
+
expect(
|
|
77
|
+
chooseStartPath({
|
|
78
|
+
hasInplaceActivateHook: false,
|
|
79
|
+
isServiceRunning: true,
|
|
80
|
+
}),
|
|
81
|
+
).toBe("reload");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("dashboard 模式 + service 未运行 → spawn", () => {
|
|
85
|
+
expect(
|
|
86
|
+
chooseStartPath({
|
|
87
|
+
hasInplaceActivateHook: false,
|
|
88
|
+
isServiceRunning: false,
|
|
89
|
+
}),
|
|
90
|
+
).toBe("spawn");
|
|
91
|
+
});
|
|
92
|
+
});
|