chatccc 0.2.52 → 0.2.53

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.
Files changed (39) hide show
  1. package/README.md +95 -256
  2. package/bin/chatccc.mjs +23 -23
  3. package/demo/ilink_echo_probe.ts +222 -222
  4. package/im-skills/feishu-skill/download-video.mjs +162 -162
  5. package/im-skills/feishu-skill/receive-send-file.md +66 -66
  6. package/im-skills/feishu-skill/receive-send-image.md +27 -27
  7. package/im-skills/feishu-skill/send-file.mjs +50 -50
  8. package/im-skills/feishu-skill/send-image.mjs +50 -50
  9. package/im-skills/feishu-skill/skill.md +10 -10
  10. package/package.json +59 -59
  11. package/src/__tests__/agent-image-rpc.test.ts +33 -33
  12. package/src/__tests__/agent-rpc-body.test.ts +41 -41
  13. package/src/__tests__/card-plain-text.test.ts +42 -42
  14. package/src/__tests__/codex-adapter.test.ts +304 -304
  15. package/src/__tests__/config-reload.test.ts +26 -26
  16. package/src/__tests__/config-sample.test.ts +19 -19
  17. package/src/__tests__/crash-logging.test.ts +62 -62
  18. package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
  19. package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
  20. package/src/__tests__/im-skills.test.ts +46 -46
  21. package/src/__tests__/session.test.ts +57 -2
  22. package/src/__tests__/sim-agent.test.ts +173 -173
  23. package/src/__tests__/sim-platform.test.ts +76 -76
  24. package/src/__tests__/sim-store.test.ts +213 -213
  25. package/src/__tests__/stream-state.test.ts +134 -134
  26. package/src/__tests__/wechat-platform.test.ts +57 -32
  27. package/src/adapters/codex-adapter.ts +294 -294
  28. package/src/adapters/codex-session-meta-store.ts +130 -130
  29. package/src/agent-file-rpc.ts +156 -156
  30. package/src/agent-image-rpc.ts +152 -152
  31. package/src/agent-rpc-body.ts +48 -48
  32. package/src/card-plain-text.ts +108 -108
  33. package/src/im-skills.ts +109 -109
  34. package/src/platform-adapter.ts +60 -60
  35. package/src/session-chat-binding.ts +6 -1
  36. package/src/session.ts +40 -35
  37. package/src/sim-agent.ts +167 -167
  38. package/src/trace.ts +50 -50
  39. package/src/wechat-platform.ts +1 -1
@@ -1,134 +1,134 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import { mkdtemp, rm, readFile, readdir, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- // 在 import stream-state 之前先 mock config,让 USER_DATA_DIR 指向临时目录
7
- const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-stream-state-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
- const {
18
- writeStreamState,
19
- readStreamState,
20
- createEmptyStreamState,
21
- STREAMS_DIR,
22
- _setRenameForTest,
23
- _resetRenameForTest,
24
- } = await import("../stream-state.ts");
25
-
26
- describe("writeStreamState — atomic rename", () => {
27
- beforeEach(async () => {
28
- // 清空测试目录
29
- try {
30
- const entries = await readdir(STREAMS_DIR);
31
- for (const e of entries) await rm(join(STREAMS_DIR, e), { force: true });
32
- } catch { /* dir not yet exist */ }
33
- });
34
-
35
- afterEach(async () => {
36
- vi.restoreAllMocks();
37
- });
38
-
39
- it("writeStreamState 写完后 readStreamState 能读到完整内容", async () => {
40
- const state = createEmptyStreamState("sid-1", "/tmp", "claude", 1);
41
- state.accumulatedContent = "hello";
42
- state.finalReply = "world";
43
-
44
- await writeStreamState(state);
45
-
46
- const got = await readStreamState("sid-1");
47
- expect(got).not.toBeNull();
48
- expect(got!.accumulatedContent).toBe("hello");
49
- expect(got!.finalReply).toBe("world");
50
- });
51
-
52
- it("写入完成后,临时 .tmp 文件不应残留(rename 成功路径)", async () => {
53
- const state = createEmptyStreamState("sid-2", "/tmp", "claude", 1);
54
- await writeStreamState(state);
55
-
56
- const entries = await readdir(STREAMS_DIR);
57
- // 只应有 sid-2.json,没有 sid-2.json.tmp
58
- expect(entries).toContain("sid-2.json");
59
- expect(entries.some((e) => e.endsWith(".tmp"))).toBe(false);
60
- });
61
-
62
- it("覆盖写入时,reader 永远只读到完整 JSON(不读到半截)", async () => {
63
- // 此测试验证:先写一个版本,再写第二个版本,reader 中间读取必然是某个完整版本
64
- const state1 = createEmptyStreamState("sid-3", "/tmp", "claude", 1);
65
- state1.accumulatedContent = "v1";
66
- await writeStreamState(state1);
67
-
68
- // 直接读原始字节,确认是完整 JSON
69
- const raw1 = await readFile(join(STREAMS_DIR, "sid-3.json"), "utf-8");
70
- expect(() => JSON.parse(raw1)).not.toThrow();
71
- expect(JSON.parse(raw1).accumulatedContent).toBe("v1");
72
-
73
- const state2 = createEmptyStreamState("sid-3", "/tmp", "claude", 2);
74
- state2.accumulatedContent = "v2";
75
- await writeStreamState(state2);
76
-
77
- const raw2 = await readFile(join(STREAMS_DIR, "sid-3.json"), "utf-8");
78
- expect(() => JSON.parse(raw2)).not.toThrow();
79
- expect(JSON.parse(raw2).accumulatedContent).toBe("v2");
80
- });
81
-
82
- it("rename 失败时降级为直接覆盖写,不抛错 + 数据仍写入", async () => {
83
- let renameCalls = 0;
84
- _setRenameForTest(async () => {
85
- renameCalls++;
86
- throw Object.assign(new Error("EPERM mocked"), { code: "EPERM" });
87
- });
88
-
89
- try {
90
- const state = createEmptyStreamState("sid-fallback", "/tmp", "claude", 1);
91
- state.accumulatedContent = "fallback-content";
92
-
93
- // 不应抛错
94
- await expect(writeStreamState(state)).resolves.toBeUndefined();
95
-
96
- // 数据仍应写入(降级路径走 writeFile 覆盖)
97
- const got = await readStreamState("sid-fallback");
98
- expect(got).not.toBeNull();
99
- expect(got!.accumulatedContent).toBe("fallback-content");
100
-
101
- expect(renameCalls).toBe(1);
102
- } finally {
103
- _resetRenameForTest();
104
- }
105
- });
106
-
107
- it("readStreamState 对损坏的 JSON 返回 null(reader 兜底契约)", async () => {
108
- // 直接写一段半截 JSON 到目标路径
109
- const filePath = join(STREAMS_DIR, "sid-broken.json");
110
- await writeFile(filePath, '{"sessionId":"sid-broken","accum', "utf-8");
111
-
112
- const got = await readStreamState("sid-broken");
113
- expect(got).toBeNull();
114
- });
115
-
116
- it("readStreamState 对不存在文件返回 null", async () => {
117
- const got = await readStreamState("never-existed");
118
- expect(got).toBeNull();
119
- });
120
- });
121
-
122
- describe("createEmptyStreamState", () => {
123
- it("生成的 state 字段正确且 status=running", () => {
124
- const s = createEmptyStreamState("sid-X", "/cwd", "cursor", 5);
125
- expect(s.sessionId).toBe("sid-X");
126
- expect(s.cwd).toBe("/cwd");
127
- expect(s.tool).toBe("cursor");
128
- expect(s.turnCount).toBe(5);
129
- expect(s.status).toBe("running");
130
- expect(s.accumulatedContent).toBe("");
131
- expect(s.finalReply).toBe("");
132
- expect(s.chunkCount).toBe(0);
133
- });
134
- });
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdtemp, rm, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ // 在 import stream-state 之前先 mock config,让 USER_DATA_DIR 指向临时目录
7
+ const TEST_DATA_DIR = await mkdtemp(join(tmpdir(), "chatccc-stream-state-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
+ const {
18
+ writeStreamState,
19
+ readStreamState,
20
+ createEmptyStreamState,
21
+ STREAMS_DIR,
22
+ _setRenameForTest,
23
+ _resetRenameForTest,
24
+ } = await import("../stream-state.ts");
25
+
26
+ describe("writeStreamState — atomic rename", () => {
27
+ beforeEach(async () => {
28
+ // 清空测试目录
29
+ try {
30
+ const entries = await readdir(STREAMS_DIR);
31
+ for (const e of entries) await rm(join(STREAMS_DIR, e), { force: true });
32
+ } catch { /* dir not yet exist */ }
33
+ });
34
+
35
+ afterEach(async () => {
36
+ vi.restoreAllMocks();
37
+ });
38
+
39
+ it("writeStreamState 写完后 readStreamState 能读到完整内容", async () => {
40
+ const state = createEmptyStreamState("sid-1", "/tmp", "claude", 1);
41
+ state.accumulatedContent = "hello";
42
+ state.finalReply = "world";
43
+
44
+ await writeStreamState(state);
45
+
46
+ const got = await readStreamState("sid-1");
47
+ expect(got).not.toBeNull();
48
+ expect(got!.accumulatedContent).toBe("hello");
49
+ expect(got!.finalReply).toBe("world");
50
+ });
51
+
52
+ it("写入完成后,临时 .tmp 文件不应残留(rename 成功路径)", async () => {
53
+ const state = createEmptyStreamState("sid-2", "/tmp", "claude", 1);
54
+ await writeStreamState(state);
55
+
56
+ const entries = await readdir(STREAMS_DIR);
57
+ // 只应有 sid-2.json,没有 sid-2.json.tmp
58
+ expect(entries).toContain("sid-2.json");
59
+ expect(entries.some((e) => e.endsWith(".tmp"))).toBe(false);
60
+ });
61
+
62
+ it("覆盖写入时,reader 永远只读到完整 JSON(不读到半截)", async () => {
63
+ // 此测试验证:先写一个版本,再写第二个版本,reader 中间读取必然是某个完整版本
64
+ const state1 = createEmptyStreamState("sid-3", "/tmp", "claude", 1);
65
+ state1.accumulatedContent = "v1";
66
+ await writeStreamState(state1);
67
+
68
+ // 直接读原始字节,确认是完整 JSON
69
+ const raw1 = await readFile(join(STREAMS_DIR, "sid-3.json"), "utf-8");
70
+ expect(() => JSON.parse(raw1)).not.toThrow();
71
+ expect(JSON.parse(raw1).accumulatedContent).toBe("v1");
72
+
73
+ const state2 = createEmptyStreamState("sid-3", "/tmp", "claude", 2);
74
+ state2.accumulatedContent = "v2";
75
+ await writeStreamState(state2);
76
+
77
+ const raw2 = await readFile(join(STREAMS_DIR, "sid-3.json"), "utf-8");
78
+ expect(() => JSON.parse(raw2)).not.toThrow();
79
+ expect(JSON.parse(raw2).accumulatedContent).toBe("v2");
80
+ });
81
+
82
+ it("rename 失败时降级为直接覆盖写,不抛错 + 数据仍写入", async () => {
83
+ let renameCalls = 0;
84
+ _setRenameForTest(async () => {
85
+ renameCalls++;
86
+ throw Object.assign(new Error("EPERM mocked"), { code: "EPERM" });
87
+ });
88
+
89
+ try {
90
+ const state = createEmptyStreamState("sid-fallback", "/tmp", "claude", 1);
91
+ state.accumulatedContent = "fallback-content";
92
+
93
+ // 不应抛错
94
+ await expect(writeStreamState(state)).resolves.toBeUndefined();
95
+
96
+ // 数据仍应写入(降级路径走 writeFile 覆盖)
97
+ const got = await readStreamState("sid-fallback");
98
+ expect(got).not.toBeNull();
99
+ expect(got!.accumulatedContent).toBe("fallback-content");
100
+
101
+ expect(renameCalls).toBe(1);
102
+ } finally {
103
+ _resetRenameForTest();
104
+ }
105
+ });
106
+
107
+ it("readStreamState 对损坏的 JSON 返回 null(reader 兜底契约)", async () => {
108
+ // 直接写一段半截 JSON 到目标路径
109
+ const filePath = join(STREAMS_DIR, "sid-broken.json");
110
+ await writeFile(filePath, '{"sessionId":"sid-broken","accum', "utf-8");
111
+
112
+ const got = await readStreamState("sid-broken");
113
+ expect(got).toBeNull();
114
+ });
115
+
116
+ it("readStreamState 对不存在文件返回 null", async () => {
117
+ const got = await readStreamState("never-existed");
118
+ expect(got).toBeNull();
119
+ });
120
+ });
121
+
122
+ describe("createEmptyStreamState", () => {
123
+ it("生成的 state 字段正确且 status=running", () => {
124
+ const s = createEmptyStreamState("sid-X", "/cwd", "cursor", 5);
125
+ expect(s.sessionId).toBe("sid-X");
126
+ expect(s.cwd).toBe("/cwd");
127
+ expect(s.tool).toBe("cursor");
128
+ expect(s.turnCount).toBe(5);
129
+ expect(s.status).toBe("running");
130
+ expect(s.accumulatedContent).toBe("");
131
+ expect(s.finalReply).toBe("");
132
+ expect(s.chunkCount).toBe(0);
133
+ });
134
+ });
@@ -1,32 +1,57 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import { buildHelpCard } from "../cards.ts";
4
- import { createWechatAdapter } from "../wechat-platform.ts";
5
-
6
- describe("createWechatAdapter", () => {
7
- it("degrades raw cards to plain text messages", async () => {
8
- const wire = {
9
- push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
10
- sendText: vi.fn(
11
- async (_chatId: string, _text: string, _contextToken?: string) =>
12
- "msg-id",
13
- ),
14
- };
15
- const log = vi.fn();
16
- const platform = createWechatAdapter({
17
- getWire: () => wire,
18
- log,
19
- });
20
-
21
- const ok = await platform.sendRawCard("wx-chat-help", buildHelpCard("Hello"));
22
-
23
- expect(ok).toBe(true);
24
- expect(wire.push).toHaveBeenCalledTimes(1);
25
- expect(wire.sendText).not.toHaveBeenCalled();
26
- const [, text] = wire.push.mock.calls[0];
27
- expect(text).toContain("# ChatCCC");
28
- expect(text).toContain("Hello");
29
- expect(text).toContain("/new");
30
- expect(log).toHaveBeenCalledWith("[WECHAT] sendRawCard degraded to text");
31
- });
32
- });
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { buildHelpCard } from "../cards.ts";
4
+ import { createWechatAdapter } from "../wechat-platform.ts";
5
+
6
+ describe("createWechatAdapter", () => {
7
+ it("degrades raw cards to plain text messages", async () => {
8
+ const wire = {
9
+ push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
10
+ sendText: vi.fn(
11
+ async (_chatId: string, _text: string, _contextToken?: string) =>
12
+ "msg-id",
13
+ ),
14
+ };
15
+ const log = vi.fn();
16
+ const platform = createWechatAdapter({
17
+ getWire: () => wire,
18
+ log,
19
+ });
20
+
21
+ const ok = await platform.sendRawCard("wx-chat-help", buildHelpCard("Hello"));
22
+
23
+ expect(ok).toBe(true);
24
+ expect(wire.push).toHaveBeenCalledTimes(1);
25
+ expect(wire.sendText).not.toHaveBeenCalled();
26
+ const [, text] = wire.push.mock.calls[0];
27
+ expect(text).toContain("# ChatCCC");
28
+ expect(text).toContain("Hello");
29
+ expect(text).toContain("/new");
30
+ expect(log).toHaveBeenCalledWith("[WECHAT] sendRawCard degraded to text");
31
+ });
32
+
33
+ it("reports unsent non-final messages after the claw limit", async () => {
34
+ const wire = {
35
+ push: vi.fn(async (_chatId: string, _text: string) => "msg-id"),
36
+ sendText: vi.fn(
37
+ async (_chatId: string, _text: string, _contextToken?: string) =>
38
+ "msg-id",
39
+ ),
40
+ };
41
+ const log = vi.fn();
42
+ const platform = createWechatAdapter({
43
+ getWire: () => wire,
44
+ log,
45
+ });
46
+
47
+ for (let i = 0; i < 10; i++) {
48
+ await expect(platform.sendText("wx-chat-limit", `chunk ${i}`)).resolves.toBe(true);
49
+ }
50
+
51
+ await expect(platform.sendText("wx-chat-limit", "chunk 10")).resolves.toBe(false);
52
+ expect(wire.push).toHaveBeenCalledTimes(10);
53
+ expect(log).toHaveBeenCalledWith(
54
+ "[WECHAT] sendText skipped (claw limit): chatId=wx-chat-limit count=11",
55
+ );
56
+ });
57
+ });