chatccc 0.2.55 → 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.
@@ -1,283 +1,283 @@
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-simplify-test-"));
7
- vi.mock("../config.ts", async () => {
8
- const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
9
- return {
10
- ...actual,
11
- PROJECT_ROOT: TEST_DATA_DIR,
12
- ts: () => "test-ts",
13
- };
14
- });
15
-
16
- let simplifyToolUse: (name: string, input: unknown) => string | null;
17
- let simplifyToolResult: (name: string, toolUseId: string, isError: boolean, toolCallInput?: unknown) => string | null;
18
- let reloadSimplifyConfig: () => void;
19
-
20
- beforeEach(async () => {
21
- vi.resetModules();
22
- try {
23
- await rm(join(TEST_DATA_DIR, "simplify.json"), { force: true });
24
- } catch {}
25
- const mod = await import("../simplify.ts");
26
- simplifyToolUse = mod.simplifyToolUse;
27
- simplifyToolResult = mod.simplifyToolResult;
28
- reloadSimplifyConfig = mod.reloadSimplifyConfig;
29
- });
30
-
31
- afterAll(async () => {
32
- try {
33
- await rm(TEST_DATA_DIR, { recursive: true, force: true });
34
- } catch {}
35
- });
36
-
37
- describe("simplifyToolUse", () => {
38
- it("无 simplify.json 时返回 null(回退默认格式化)", () => {
39
- reloadSimplifyConfig();
40
- expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" })).toBeNull();
41
- });
42
-
43
- it("有规则时按模板格式化 tool_use", async () => {
44
- await writeFile(
45
- join(TEST_DATA_DIR, "simplify.json"),
46
- JSON.stringify({
47
- tool_use: {
48
- Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
49
- },
50
- }),
51
- "utf-8",
52
- );
53
- reloadSimplifyConfig();
54
-
55
- expect(simplifyToolUse("Read", { file_path: "/home/user/project/src/index.ts" }))
56
- .toBe("📖 **Read** /home/user/project/src/index.ts");
57
- });
58
-
59
- it("无对应工具规则时返回 null", async () => {
60
- await writeFile(
61
- join(TEST_DATA_DIR, "simplify.json"),
62
- JSON.stringify({
63
- tool_use: {
64
- Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
65
- },
66
- }),
67
- "utf-8",
68
- );
69
- reloadSimplifyConfig();
70
-
71
- expect(simplifyToolUse("Write", { file_path: "/tmp/x.ts" })).toBeNull();
72
- });
73
-
74
- it("模板中未匹配的占位符保留原文", async () => {
75
- await writeFile(
76
- join(TEST_DATA_DIR, "simplify.json"),
77
- JSON.stringify({
78
- tool_use: {
79
- Read: { template: "📖 **Read** {file_path} {unknown_field}", maxLength: 300 },
80
- },
81
- }),
82
- "utf-8",
83
- );
84
- reloadSimplifyConfig();
85
-
86
- expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" }))
87
- .toBe("📖 **Read** /tmp/x.ts {unknown_field}");
88
- });
89
-
90
- it("超过 maxLength 时截断", async () => {
91
- await writeFile(
92
- join(TEST_DATA_DIR, "simplify.json"),
93
- JSON.stringify({
94
- tool_use: {
95
- Bash: { template: "🖥️ **Bash** {command}", maxLength: 20 },
96
- },
97
- }),
98
- "utf-8",
99
- );
100
- reloadSimplifyConfig();
101
-
102
- const result = simplifyToolUse("Bash", { command: "echo hello world this is a long command" });
103
- expect(result).toBeTruthy();
104
- // 被截断了:长度不超过 maxLength + 3("..." 后缀)
105
- expect(result!.length).toBeLessThanOrEqual(23);
106
- expect(result!.endsWith("...")).toBe(true);
107
- });
108
-
109
- it("多字段模板", async () => {
110
- await writeFile(
111
- join(TEST_DATA_DIR, "simplify.json"),
112
- JSON.stringify({
113
- tool_use: {
114
- Grep: { template: "🔎 **Grep** {pattern} in {path}", maxLength: 300 },
115
- },
116
- }),
117
- "utf-8",
118
- );
119
- reloadSimplifyConfig();
120
-
121
- expect(simplifyToolUse("Grep", { pattern: "TODO", path: "src/" }))
122
- .toBe("🔎 **Grep** TODO in src/");
123
- });
124
-
125
- it("input 为 null 时不抛异常", async () => {
126
- await writeFile(
127
- join(TEST_DATA_DIR, "simplify.json"),
128
- JSON.stringify({
129
- tool_use: {
130
- TodoWrite: { template: "✅ **TodoWrite**", maxLength: 200 },
131
- },
132
- }),
133
- "utf-8",
134
- );
135
- reloadSimplifyConfig();
136
-
137
- expect(simplifyToolUse("TodoWrite", null)).toBe("✅ **TodoWrite**");
138
- });
139
- });
140
-
141
- describe("simplifyToolResult", () => {
142
- it("无 simplify.json 时返回 null", () => {
143
- reloadSimplifyConfig();
144
- expect(simplifyToolResult("Read", "abc123def456", false)).toBeNull();
145
- });
146
-
147
- it("有规则时按模板格式化 tool_result", async () => {
148
- await writeFile(
149
- join(TEST_DATA_DIR, "simplify.json"),
150
- JSON.stringify({
151
- tool_result: {
152
- Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
153
- },
154
- }),
155
- "utf-8",
156
- );
157
- reloadSimplifyConfig();
158
-
159
- expect(simplifyToolResult("Read", "abc123def456", false))
160
- .toBe("✅ *def456*: 已读取");
161
- });
162
-
163
- it("isError 时结果前加 ❌", async () => {
164
- await writeFile(
165
- join(TEST_DATA_DIR, "simplify.json"),
166
- JSON.stringify({
167
- tool_result: {
168
- Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
169
- },
170
- }),
171
- "utf-8",
172
- );
173
- reloadSimplifyConfig();
174
-
175
- expect(simplifyToolResult("Read", "abc123def456", true))
176
- .toBe("❌ ✅ *def456*: 已读取");
177
- });
178
-
179
- it("tool_result 模板可使用 tool_use 的输入字段", async () => {
180
- await writeFile(
181
- join(TEST_DATA_DIR, "simplify.json"),
182
- JSON.stringify({
183
- tool_result: {
184
- Write: { template: "✅ *{id}*: 已写入 {file_path}", maxLength: 200 },
185
- },
186
- }),
187
- "utf-8",
188
- );
189
- reloadSimplifyConfig();
190
-
191
- expect(simplifyToolResult("Write", "abc123def456", false, { file_path: "/tmp/out.txt" }))
192
- .toBe("✅ *def456*: 已写入 /tmp/out.txt");
193
- });
194
-
195
- it("无对应工具规则时返回 null", async () => {
196
- await writeFile(
197
- join(TEST_DATA_DIR, "simplify.json"),
198
- JSON.stringify({
199
- tool_result: {
200
- Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
201
- },
202
- }),
203
- "utf-8",
204
- );
205
- reloadSimplifyConfig();
206
-
207
- expect(simplifyToolResult("Write", "abc123def456", false)).toBeNull();
208
- });
209
- });
210
-
211
- describe("accumulateBlockContent with simplification", () => {
212
- it("tool_use 使用简化规则,tool_result 回退默认格式", async () => {
213
- // 动态 import session 以使用当前 mock 的简单化模块
214
- const { accumulateBlockContent } = await import("../session.ts");
215
-
216
- await writeFile(
217
- join(TEST_DATA_DIR, "simplify.json"),
218
- JSON.stringify({
219
- tool_use: {
220
- Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
221
- },
222
- }),
223
- "utf-8",
224
- );
225
- reloadSimplifyConfig();
226
-
227
- const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
228
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
229
-
230
- accumulateBlockContent(
231
- { type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
232
- state,
233
- toolCallMap,
234
- );
235
-
236
- expect(state.accumulatedContent).toBe("\n\n📖 **Read** /src/app.ts\n");
237
- });
238
-
239
- it("无简化规则时回退默认 tool_use 格式", async () => {
240
- const { accumulateBlockContent: acb } = await import("../session.ts");
241
-
242
- reloadSimplifyConfig(); // 无 simplify.json
243
-
244
- const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
245
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
246
-
247
- acb(
248
- { type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
249
- state,
250
- toolCallMap,
251
- );
252
-
253
- expect(state.accumulatedContent).toContain("📖 **Read**");
254
- expect(state.accumulatedContent).toContain('{"file_path":"/src/app.ts"}');
255
- });
256
-
257
- it("tool_result 使用简化规则", async () => {
258
- const { accumulateBlockContent: acb } = await import("../session.ts");
259
-
260
- await writeFile(
261
- join(TEST_DATA_DIR, "simplify.json"),
262
- JSON.stringify({
263
- tool_result: {
264
- Read: { template: "✅ *{id}*: 已读取 {file_path}", maxLength: 200 },
265
- },
266
- }),
267
- "utf-8",
268
- );
269
- reloadSimplifyConfig();
270
-
271
- const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
272
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
273
- toolCallMap.set("toolu_001", { name: "Read", input: { file_path: "/src/app.ts" } });
274
-
275
- acb(
276
- { type: "tool_result", tool_use_id: "toolu_001", content: "file content here..." },
277
- state,
278
- toolCallMap,
279
- );
280
-
281
- expect(state.accumulatedContent).toBe("✅ *lu_001*: 已读取 /src/app.ts\n");
282
- });
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-simplify-test-"));
7
+ vi.mock("../config.ts", async () => {
8
+ const actual = await vi.importActual<typeof import("../config.ts")>("../config.ts");
9
+ return {
10
+ ...actual,
11
+ PROJECT_ROOT: TEST_DATA_DIR,
12
+ ts: () => "test-ts",
13
+ };
14
+ });
15
+
16
+ let simplifyToolUse: (name: string, input: unknown) => string | null;
17
+ let simplifyToolResult: (name: string, toolUseId: string, isError: boolean, toolCallInput?: unknown) => string | null;
18
+ let reloadSimplifyConfig: () => void;
19
+
20
+ beforeEach(async () => {
21
+ vi.resetModules();
22
+ try {
23
+ await rm(join(TEST_DATA_DIR, "simplify.json"), { force: true });
24
+ } catch {}
25
+ const mod = await import("../simplify.ts");
26
+ simplifyToolUse = mod.simplifyToolUse;
27
+ simplifyToolResult = mod.simplifyToolResult;
28
+ reloadSimplifyConfig = mod.reloadSimplifyConfig;
29
+ });
30
+
31
+ afterAll(async () => {
32
+ try {
33
+ await rm(TEST_DATA_DIR, { recursive: true, force: true });
34
+ } catch {}
35
+ });
36
+
37
+ describe("simplifyToolUse", () => {
38
+ it("无 simplify.json 时返回 null(回退默认格式化)", () => {
39
+ reloadSimplifyConfig();
40
+ expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" })).toBeNull();
41
+ });
42
+
43
+ it("有规则时按模板格式化 tool_use", async () => {
44
+ await writeFile(
45
+ join(TEST_DATA_DIR, "simplify.json"),
46
+ JSON.stringify({
47
+ tool_use: {
48
+ Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
49
+ },
50
+ }),
51
+ "utf-8",
52
+ );
53
+ reloadSimplifyConfig();
54
+
55
+ expect(simplifyToolUse("Read", { file_path: "/home/user/project/src/index.ts" }))
56
+ .toBe("📖 **Read** /home/user/project/src/index.ts");
57
+ });
58
+
59
+ it("无对应工具规则时返回 null", async () => {
60
+ await writeFile(
61
+ join(TEST_DATA_DIR, "simplify.json"),
62
+ JSON.stringify({
63
+ tool_use: {
64
+ Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
65
+ },
66
+ }),
67
+ "utf-8",
68
+ );
69
+ reloadSimplifyConfig();
70
+
71
+ expect(simplifyToolUse("Write", { file_path: "/tmp/x.ts" })).toBeNull();
72
+ });
73
+
74
+ it("模板中未匹配的占位符保留原文", async () => {
75
+ await writeFile(
76
+ join(TEST_DATA_DIR, "simplify.json"),
77
+ JSON.stringify({
78
+ tool_use: {
79
+ Read: { template: "📖 **Read** {file_path} {unknown_field}", maxLength: 300 },
80
+ },
81
+ }),
82
+ "utf-8",
83
+ );
84
+ reloadSimplifyConfig();
85
+
86
+ expect(simplifyToolUse("Read", { file_path: "/tmp/x.ts" }))
87
+ .toBe("📖 **Read** /tmp/x.ts {unknown_field}");
88
+ });
89
+
90
+ it("超过 maxLength 时截断", async () => {
91
+ await writeFile(
92
+ join(TEST_DATA_DIR, "simplify.json"),
93
+ JSON.stringify({
94
+ tool_use: {
95
+ Bash: { template: "🖥️ **Bash** {command}", maxLength: 20 },
96
+ },
97
+ }),
98
+ "utf-8",
99
+ );
100
+ reloadSimplifyConfig();
101
+
102
+ const result = simplifyToolUse("Bash", { command: "echo hello world this is a long command" });
103
+ expect(result).toBeTruthy();
104
+ // 被截断了:长度不超过 maxLength + 3("..." 后缀)
105
+ expect(result!.length).toBeLessThanOrEqual(23);
106
+ expect(result!.endsWith("...")).toBe(true);
107
+ });
108
+
109
+ it("多字段模板", async () => {
110
+ await writeFile(
111
+ join(TEST_DATA_DIR, "simplify.json"),
112
+ JSON.stringify({
113
+ tool_use: {
114
+ Grep: { template: "🔎 **Grep** {pattern} in {path}", maxLength: 300 },
115
+ },
116
+ }),
117
+ "utf-8",
118
+ );
119
+ reloadSimplifyConfig();
120
+
121
+ expect(simplifyToolUse("Grep", { pattern: "TODO", path: "src/" }))
122
+ .toBe("🔎 **Grep** TODO in src/");
123
+ });
124
+
125
+ it("input 为 null 时不抛异常", async () => {
126
+ await writeFile(
127
+ join(TEST_DATA_DIR, "simplify.json"),
128
+ JSON.stringify({
129
+ tool_use: {
130
+ TodoWrite: { template: "✅ **TodoWrite**", maxLength: 200 },
131
+ },
132
+ }),
133
+ "utf-8",
134
+ );
135
+ reloadSimplifyConfig();
136
+
137
+ expect(simplifyToolUse("TodoWrite", null)).toBe("✅ **TodoWrite**");
138
+ });
139
+ });
140
+
141
+ describe("simplifyToolResult", () => {
142
+ it("无 simplify.json 时返回 null", () => {
143
+ reloadSimplifyConfig();
144
+ expect(simplifyToolResult("Read", "abc123def456", false)).toBeNull();
145
+ });
146
+
147
+ it("有规则时按模板格式化 tool_result", async () => {
148
+ await writeFile(
149
+ join(TEST_DATA_DIR, "simplify.json"),
150
+ JSON.stringify({
151
+ tool_result: {
152
+ Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
153
+ },
154
+ }),
155
+ "utf-8",
156
+ );
157
+ reloadSimplifyConfig();
158
+
159
+ expect(simplifyToolResult("Read", "abc123def456", false))
160
+ .toBe("✅ *def456*: 已读取");
161
+ });
162
+
163
+ it("isError 时结果前加 ❌", async () => {
164
+ await writeFile(
165
+ join(TEST_DATA_DIR, "simplify.json"),
166
+ JSON.stringify({
167
+ tool_result: {
168
+ Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
169
+ },
170
+ }),
171
+ "utf-8",
172
+ );
173
+ reloadSimplifyConfig();
174
+
175
+ expect(simplifyToolResult("Read", "abc123def456", true))
176
+ .toBe("❌ ✅ *def456*: 已读取");
177
+ });
178
+
179
+ it("tool_result 模板可使用 tool_use 的输入字段", async () => {
180
+ await writeFile(
181
+ join(TEST_DATA_DIR, "simplify.json"),
182
+ JSON.stringify({
183
+ tool_result: {
184
+ Write: { template: "✅ *{id}*: 已写入 {file_path}", maxLength: 200 },
185
+ },
186
+ }),
187
+ "utf-8",
188
+ );
189
+ reloadSimplifyConfig();
190
+
191
+ expect(simplifyToolResult("Write", "abc123def456", false, { file_path: "/tmp/out.txt" }))
192
+ .toBe("✅ *def456*: 已写入 /tmp/out.txt");
193
+ });
194
+
195
+ it("无对应工具规则时返回 null", async () => {
196
+ await writeFile(
197
+ join(TEST_DATA_DIR, "simplify.json"),
198
+ JSON.stringify({
199
+ tool_result: {
200
+ Read: { template: "✅ *{id}*: 已读取", maxLength: 200 },
201
+ },
202
+ }),
203
+ "utf-8",
204
+ );
205
+ reloadSimplifyConfig();
206
+
207
+ expect(simplifyToolResult("Write", "abc123def456", false)).toBeNull();
208
+ });
209
+ });
210
+
211
+ describe("accumulateBlockContent with simplification", () => {
212
+ it("tool_use 使用简化规则,tool_result 回退默认格式", async () => {
213
+ // 动态 import session 以使用当前 mock 的简单化模块
214
+ const { accumulateBlockContent } = await import("../session.ts");
215
+
216
+ await writeFile(
217
+ join(TEST_DATA_DIR, "simplify.json"),
218
+ JSON.stringify({
219
+ tool_use: {
220
+ Read: { template: "📖 **Read** {file_path}", maxLength: 300 },
221
+ },
222
+ }),
223
+ "utf-8",
224
+ );
225
+ reloadSimplifyConfig();
226
+
227
+ const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
228
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
229
+
230
+ accumulateBlockContent(
231
+ { type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
232
+ state,
233
+ toolCallMap,
234
+ );
235
+
236
+ expect(state.accumulatedContent).toBe("\n\n📖 **Read** /src/app.ts\n");
237
+ });
238
+
239
+ it("无简化规则时回退默认 tool_use 格式", async () => {
240
+ const { accumulateBlockContent: acb } = await import("../session.ts");
241
+
242
+ reloadSimplifyConfig(); // 无 simplify.json
243
+
244
+ const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
245
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
246
+
247
+ acb(
248
+ { type: "tool_use", id: "toolu_001", name: "Read", input: { file_path: "/src/app.ts" } },
249
+ state,
250
+ toolCallMap,
251
+ );
252
+
253
+ expect(state.accumulatedContent).toContain("📖 **Read**");
254
+ expect(state.accumulatedContent).toContain('{"file_path":"/src/app.ts"}');
255
+ });
256
+
257
+ it("tool_result 使用简化规则", async () => {
258
+ const { accumulateBlockContent: acb } = await import("../session.ts");
259
+
260
+ await writeFile(
261
+ join(TEST_DATA_DIR, "simplify.json"),
262
+ JSON.stringify({
263
+ tool_result: {
264
+ Read: { template: "✅ *{id}*: 已读取 {file_path}", maxLength: 200 },
265
+ },
266
+ }),
267
+ "utf-8",
268
+ );
269
+ reloadSimplifyConfig();
270
+
271
+ const state = { accumulatedContent: "", finalText: "", finalCompleteText: "", chunkCount: 0 };
272
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
273
+ toolCallMap.set("toolu_001", { name: "Read", input: { file_path: "/src/app.ts" } });
274
+
275
+ acb(
276
+ { type: "tool_result", tool_use_id: "toolu_001", content: "file content here..." },
277
+ state,
278
+ toolCallMap,
279
+ );
280
+
281
+ expect(state.accumulatedContent).toBe("✅ *lu_001*: 已读取 /src/app.ts\n");
282
+ });
283
283
  });
@@ -1,9 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import {
3
- applyClaudeApiMode,
4
- chooseStartPath,
5
- detectClaudeApiMode,
6
- } from "../web-ui.ts";
3
+ applyClaudeApiMode,
4
+ chooseStartPath,
5
+ detectClaudeApiMode,
6
+ unflattenConfig,
7
+ } from "../web-ui.ts";
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // detectClaudeApiMode — 加载已有 config 时如何判定 UI 初始模式
@@ -56,7 +57,7 @@ describe("detectClaudeApiMode", () => {
56
57
  // - mode 未传 默认按 official 兌底(不保留可能误填的密钥)
57
58
  // ---------------------------------------------------------------------------
58
59
 
59
- describe("applyClaudeApiMode", () => {
60
+ describe("applyClaudeApiMode", () => {
60
61
  it("mode=official 时清空 CLAUDE_API_KEY / CLAUDE_BASE_URL(即使 vars 没传)", () => {
61
62
  const out = applyClaudeApiMode({ CHATCCC_APP_ID: "x" }, "official");
62
63
  expect(out).toEqual({
@@ -127,7 +128,23 @@ describe("applyClaudeApiMode", () => {
127
128
  expect(input).toEqual({ CLAUDE_API_KEY: "sk-x" }); // 原对象未变
128
129
  expect(out).not.toBe(input);
129
130
  });
130
- });
131
+ });
132
+
133
+ describe("unflattenConfig", () => {
134
+ it("maps Claude subagent model into claude.subagentModel", () => {
135
+ expect(
136
+ unflattenConfig({
137
+ CHATCCC_ANTHROPIC_MODEL: "claude-sonnet-4-6",
138
+ CHATCCC_ANTHROPIC_SUBAGENT_MODEL: "claude-haiku-4-5-20251001",
139
+ }),
140
+ ).toEqual({
141
+ claude: {
142
+ model: "claude-sonnet-4-6",
143
+ subagentModel: "claude-haiku-4-5-20251001",
144
+ },
145
+ });
146
+ });
147
+ });
131
148
 
132
149
  // ---------------------------------------------------------------------------
133
150
  // chooseStartPath — /api/start 的路径选择
@@ -29,4 +29,29 @@ describe("createWechatAdapter", () => {
29
29
  expect(text).toContain("/new");
30
30
  expect(log).toHaveBeenCalledWith("[WECHAT] sendRawCard degraded to text");
31
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
+ });
32
57
  });