@ricky-stevens/context-guardian 2.1.0
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/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +63 -0
- package/.github/workflows/ci.yml +66 -0
- package/CLAUDE.md +132 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/biome.json +34 -0
- package/bun.lock +31 -0
- package/hooks/precompact.mjs +73 -0
- package/hooks/session-start.mjs +133 -0
- package/hooks/stop.mjs +172 -0
- package/hooks/submit.mjs +133 -0
- package/lib/checkpoint.mjs +258 -0
- package/lib/compact-cli.mjs +124 -0
- package/lib/compact-output.mjs +350 -0
- package/lib/config.mjs +40 -0
- package/lib/content.mjs +33 -0
- package/lib/diagnostics.mjs +221 -0
- package/lib/estimate.mjs +254 -0
- package/lib/extract-helpers.mjs +869 -0
- package/lib/handoff.mjs +329 -0
- package/lib/logger.mjs +34 -0
- package/lib/mcp-tools.mjs +200 -0
- package/lib/paths.mjs +90 -0
- package/lib/stats.mjs +81 -0
- package/lib/statusline.mjs +123 -0
- package/lib/synthetic-session.mjs +273 -0
- package/lib/tokens.mjs +170 -0
- package/lib/tool-summary.mjs +399 -0
- package/lib/transcript.mjs +939 -0
- package/lib/trim.mjs +158 -0
- package/package.json +22 -0
- package/skills/compact/SKILL.md +20 -0
- package/skills/config/SKILL.md +70 -0
- package/skills/handoff/SKILL.md +26 -0
- package/skills/prune/SKILL.md +20 -0
- package/skills/stats/SKILL.md +100 -0
- package/sonar-project.properties +12 -0
- package/test/checkpoint.test.mjs +171 -0
- package/test/compact-cli.test.mjs +230 -0
- package/test/compact-output.test.mjs +284 -0
- package/test/compaction-e2e.test.mjs +809 -0
- package/test/content.test.mjs +86 -0
- package/test/diagnostics.test.mjs +188 -0
- package/test/edge-cases.test.mjs +543 -0
- package/test/estimate.test.mjs +262 -0
- package/test/extract-helpers-coverage.test.mjs +333 -0
- package/test/extract-helpers.test.mjs +234 -0
- package/test/handoff.test.mjs +738 -0
- package/test/integration.test.mjs +582 -0
- package/test/logger.test.mjs +70 -0
- package/test/manual-compaction-test.md +426 -0
- package/test/mcp-tools.test.mjs +443 -0
- package/test/paths.test.mjs +250 -0
- package/test/quick-compaction-test.md +191 -0
- package/test/stats.test.mjs +88 -0
- package/test/statusline.test.mjs +222 -0
- package/test/submit.test.mjs +232 -0
- package/test/synthetic-session.test.mjs +600 -0
- package/test/tokens.test.mjs +293 -0
- package/test/tool-summary.test.mjs +771 -0
- package/test/transcript-coverage.test.mjs +369 -0
- package/test/transcript.test.mjs +596 -0
- package/test/trim.test.mjs +356 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
contentBlockPlaceholder,
|
|
5
|
+
formatEditDiff,
|
|
6
|
+
summarizeToolResult,
|
|
7
|
+
summarizeToolUse,
|
|
8
|
+
} from "../lib/tool-summary.mjs";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** Generate a string of exactly N characters. */
|
|
15
|
+
function chars(n, ch = "x") {
|
|
16
|
+
return ch.repeat(n);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Build a tool_use block. */
|
|
20
|
+
function toolUse(name, input, id = "t1") {
|
|
21
|
+
return { type: "tool_use", id, name, input };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build a tool_result block with string content. */
|
|
25
|
+
function toolResult(content, toolUseId = "t1") {
|
|
26
|
+
return { type: "tool_result", tool_use_id: toolUseId, content };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Build a tool_result block with array content. */
|
|
30
|
+
function toolResultArray(texts, toolUseId = "t1") {
|
|
31
|
+
return {
|
|
32
|
+
type: "tool_result",
|
|
33
|
+
tool_use_id: toolUseId,
|
|
34
|
+
content: texts.map((t) => ({ type: "text", text: t })),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ===========================================================================
|
|
39
|
+
// summarizeToolUse
|
|
40
|
+
// ===========================================================================
|
|
41
|
+
|
|
42
|
+
describe("summarizeToolUse", () => {
|
|
43
|
+
// ── 1. Edit tool ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("Edit tool", () => {
|
|
46
|
+
it("produces old:/new: format with file path for small edits", () => {
|
|
47
|
+
const block = toolUse("Edit", {
|
|
48
|
+
file_path: "app.js",
|
|
49
|
+
old_string: "const x = 1;",
|
|
50
|
+
new_string: "const x = 2;",
|
|
51
|
+
});
|
|
52
|
+
const result = summarizeToolUse(block);
|
|
53
|
+
assert.ok(result.includes("Edit `app.js`"));
|
|
54
|
+
assert.ok(result.includes("old:"));
|
|
55
|
+
assert.ok(result.includes("new:"));
|
|
56
|
+
assert.ok(result.includes("const x = 1;"));
|
|
57
|
+
assert.ok(result.includes("const x = 2;"));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("trims large edits (>3000 chars total)", () => {
|
|
61
|
+
const big = chars(3500);
|
|
62
|
+
const block = toolUse("Edit", {
|
|
63
|
+
file_path: "big.js",
|
|
64
|
+
old_string: big,
|
|
65
|
+
new_string: big,
|
|
66
|
+
});
|
|
67
|
+
const result = summarizeToolUse(block);
|
|
68
|
+
assert.ok(result.includes("Edit `big.js`"));
|
|
69
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
70
|
+
assert.ok(result.length < big.length * 2);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── 2. Write tool ─────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("Write tool", () => {
|
|
77
|
+
it("includes file path and full content for small writes", () => {
|
|
78
|
+
const block = toolUse("Write", {
|
|
79
|
+
file_path: "out.txt",
|
|
80
|
+
content: "hello world",
|
|
81
|
+
});
|
|
82
|
+
const result = summarizeToolUse(block);
|
|
83
|
+
assert.ok(result.includes("Write `out.txt`"));
|
|
84
|
+
assert.ok(result.includes("hello world"));
|
|
85
|
+
assert.ok(!result.includes("chars)"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("trims large content with char count", () => {
|
|
89
|
+
const big = chars(5000);
|
|
90
|
+
const block = toolUse("Write", {
|
|
91
|
+
file_path: "big.txt",
|
|
92
|
+
content: big,
|
|
93
|
+
});
|
|
94
|
+
const result = summarizeToolUse(block);
|
|
95
|
+
assert.ok(result.includes("Write `big.txt`"));
|
|
96
|
+
assert.ok(result.includes("5000 chars"));
|
|
97
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── 3. Read tool ──────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("Read tool", () => {
|
|
104
|
+
it("emits note-only with file path", () => {
|
|
105
|
+
const block = toolUse("Read", { file_path: "config.json" });
|
|
106
|
+
const result = summarizeToolUse(block);
|
|
107
|
+
assert.ok(result.includes("Read `config.json`"));
|
|
108
|
+
// Should be a short note, not contain file content
|
|
109
|
+
assert.ok(result.split("\n").length <= 2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("includes offset info when present", () => {
|
|
113
|
+
const block = toolUse("Read", { file_path: "big.js", offset: 100 });
|
|
114
|
+
const result = summarizeToolUse(block);
|
|
115
|
+
assert.ok(result.includes("from line 100"));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── 4. Bash tool ──────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("Bash tool", () => {
|
|
122
|
+
it("keeps short commands in full", () => {
|
|
123
|
+
const block = toolUse("Bash", { command: "bun test" });
|
|
124
|
+
const result = summarizeToolUse(block);
|
|
125
|
+
assert.ok(result.includes("bun test"));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("trims very long heredoc commands (>3000 chars)", () => {
|
|
129
|
+
const longCmd = `cat <<'EOF'\n${chars(4000)}\nEOF`;
|
|
130
|
+
const block = toolUse("Bash", { command: longCmd });
|
|
131
|
+
const result = summarizeToolUse(block);
|
|
132
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
133
|
+
assert.ok(result.length < longCmd.length);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── 5. Grep tool ──────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("Grep tool", () => {
|
|
140
|
+
it("emits pattern and path", () => {
|
|
141
|
+
const block = toolUse("Grep", {
|
|
142
|
+
pattern: "TODO",
|
|
143
|
+
path: "src/",
|
|
144
|
+
});
|
|
145
|
+
const result = summarizeToolUse(block);
|
|
146
|
+
assert.ok(result.includes("Grep `TODO`"));
|
|
147
|
+
assert.ok(result.includes("`src/`"));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("works without path", () => {
|
|
151
|
+
const block = toolUse("Grep", { pattern: "fixme" });
|
|
152
|
+
const result = summarizeToolUse(block);
|
|
153
|
+
assert.ok(result.includes("Grep `fixme`"));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── 6. Glob tool ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
describe("Glob tool", () => {
|
|
160
|
+
it("emits pattern", () => {
|
|
161
|
+
const block = toolUse("Glob", { pattern: "**/*.mjs" });
|
|
162
|
+
const result = summarizeToolUse(block);
|
|
163
|
+
assert.ok(result.includes("Glob `**/*.mjs`"));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── 7. Agent tool ─────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe("Agent tool", () => {
|
|
170
|
+
it("includes description", () => {
|
|
171
|
+
const block = toolUse("Agent", {
|
|
172
|
+
description: "Find all usages of deprecated API",
|
|
173
|
+
});
|
|
174
|
+
const result = summarizeToolUse(block);
|
|
175
|
+
assert.ok(result.includes("Agent:"));
|
|
176
|
+
assert.ok(result.includes("Find all usages of deprecated API"));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── 8. AskUserQuestion ────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
describe("AskUserQuestion", () => {
|
|
183
|
+
it("includes question text", () => {
|
|
184
|
+
const block = toolUse("AskUserQuestion", {
|
|
185
|
+
question: "Should I proceed with the refactor?",
|
|
186
|
+
});
|
|
187
|
+
const result = summarizeToolUse(block);
|
|
188
|
+
assert.ok(result.includes("Asked user:"));
|
|
189
|
+
assert.ok(result.includes("Should I proceed with the refactor?"));
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ── 9. WebSearch ──────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
describe("WebSearch", () => {
|
|
196
|
+
it("includes query", () => {
|
|
197
|
+
const block = toolUse("WebSearch", { query: "node.js streams" });
|
|
198
|
+
const result = summarizeToolUse(block);
|
|
199
|
+
assert.ok(result.includes("WebSearch:"));
|
|
200
|
+
assert.ok(result.includes("node.js streams"));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── 10. WebFetch ─────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
describe("WebFetch", () => {
|
|
207
|
+
it("includes URL", () => {
|
|
208
|
+
const block = toolUse("WebFetch", {
|
|
209
|
+
url: "https://example.com/api",
|
|
210
|
+
});
|
|
211
|
+
const result = summarizeToolUse(block);
|
|
212
|
+
assert.ok(result.includes("WebFetch:"));
|
|
213
|
+
assert.ok(result.includes("https://example.com/api"));
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── 11. NotebookEdit ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe("NotebookEdit", () => {
|
|
220
|
+
it("preserves small cell content", () => {
|
|
221
|
+
const block = toolUse("NotebookEdit", {
|
|
222
|
+
new_source: "print('hello')",
|
|
223
|
+
});
|
|
224
|
+
const result = summarizeToolUse(block);
|
|
225
|
+
assert.ok(result.includes("NotebookEdit cell"));
|
|
226
|
+
assert.ok(result.includes("print('hello')"));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("trims large cells", () => {
|
|
230
|
+
const big = chars(5000);
|
|
231
|
+
const block = toolUse("NotebookEdit", { new_source: big });
|
|
232
|
+
const result = summarizeToolUse(block);
|
|
233
|
+
assert.ok(result.includes("NotebookEdit cell"));
|
|
234
|
+
assert.ok(result.includes("5000 chars"));
|
|
235
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── 12. Serena replace_symbol_body ────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("Serena replace_symbol_body", () => {
|
|
242
|
+
it("includes symbol name, path, and code body", () => {
|
|
243
|
+
const block = toolUse("mcp__serena__replace_symbol_body", {
|
|
244
|
+
name_path: "MyClass.myMethod",
|
|
245
|
+
relative_path: "src/foo.mjs",
|
|
246
|
+
new_body: "return 42;",
|
|
247
|
+
});
|
|
248
|
+
const result = summarizeToolUse(block);
|
|
249
|
+
assert.ok(result.includes("replaced `MyClass.myMethod`"));
|
|
250
|
+
assert.ok(result.includes("`src/foo.mjs`"));
|
|
251
|
+
assert.ok(result.includes("return 42;"));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── 13. Serena insert_after_symbol ────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
describe("Serena insert_after_symbol", () => {
|
|
258
|
+
it("preserves inserted code", () => {
|
|
259
|
+
const block = toolUse("mcp__serena__insert_after_symbol", {
|
|
260
|
+
name_path: "MyClass",
|
|
261
|
+
code: "function newHelper() {}",
|
|
262
|
+
});
|
|
263
|
+
const result = summarizeToolUse(block);
|
|
264
|
+
assert.ok(result.includes("inserted after `MyClass`"));
|
|
265
|
+
assert.ok(result.includes("function newHelper() {}"));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── 14. Serena write_memory ──────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
describe("Serena write_memory", () => {
|
|
272
|
+
it("note only with name", () => {
|
|
273
|
+
const block = toolUse("mcp__serena__write_memory", {
|
|
274
|
+
name: "architecture-notes",
|
|
275
|
+
});
|
|
276
|
+
const result = summarizeToolUse(block);
|
|
277
|
+
assert.ok(result.includes("wrote memory"));
|
|
278
|
+
assert.ok(result.includes("architecture-notes"));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ── 15. Serena find_symbol ───────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe("Serena find_symbol", () => {
|
|
285
|
+
it("note only with query", () => {
|
|
286
|
+
const block = toolUse("mcp__serena__find_symbol", {
|
|
287
|
+
name_path: "extractConversation",
|
|
288
|
+
});
|
|
289
|
+
const result = summarizeToolUse(block);
|
|
290
|
+
assert.ok(result.includes("find symbol"));
|
|
291
|
+
assert.ok(result.includes("extractConversation"));
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── 16. Serena onboarding ────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
describe("Serena onboarding", () => {
|
|
298
|
+
it("returns null (removed)", () => {
|
|
299
|
+
const block = toolUse("mcp__serena__onboarding", {});
|
|
300
|
+
const result = summarizeToolUse(block);
|
|
301
|
+
assert.equal(result, null);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ── 17. Serena rename_symbol ─────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
describe("Serena rename_symbol", () => {
|
|
308
|
+
it("shows old -> new name", () => {
|
|
309
|
+
const block = toolUse("mcp__serena__rename_symbol", {
|
|
310
|
+
old_name: "oldFunc",
|
|
311
|
+
new_name: "newFunc",
|
|
312
|
+
});
|
|
313
|
+
const result = summarizeToolUse(block);
|
|
314
|
+
assert.ok(result.includes("`oldFunc`"));
|
|
315
|
+
assert.ok(result.includes("`newFunc`"));
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ── 18. Sequential thinking ──────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe("Sequential thinking", () => {
|
|
322
|
+
it("includes step N/M and thought text", () => {
|
|
323
|
+
const block = toolUse("mcp__sequential-thinking__sequentialthinking", {
|
|
324
|
+
thought: "Let me consider the trade-offs here.",
|
|
325
|
+
thoughtNumber: 2,
|
|
326
|
+
totalThoughts: 5,
|
|
327
|
+
});
|
|
328
|
+
const result = summarizeToolUse(block);
|
|
329
|
+
assert.ok(result.includes("step 2/5"));
|
|
330
|
+
assert.ok(result.includes("Let me consider the trade-offs here."));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("trims long thoughts", () => {
|
|
334
|
+
const longThought = chars(3000);
|
|
335
|
+
const block = toolUse("mcp__sequential-thinking__sequentialthinking", {
|
|
336
|
+
thought: longThought,
|
|
337
|
+
thoughtNumber: 1,
|
|
338
|
+
totalThoughts: 1,
|
|
339
|
+
});
|
|
340
|
+
const result = summarizeToolUse(block);
|
|
341
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
342
|
+
assert.ok(result.length < longThought.length);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ── 19. Context-mode execute ─────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
describe("Context-mode execute", () => {
|
|
349
|
+
it("note only with language", () => {
|
|
350
|
+
const block = toolUse("mcp__plugin_context-mode_context-mode__execute", {
|
|
351
|
+
language: "python",
|
|
352
|
+
});
|
|
353
|
+
const result = summarizeToolUse(block);
|
|
354
|
+
assert.ok(result.includes("Context-mode:"));
|
|
355
|
+
assert.ok(result.includes("python"));
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ── 20. Context-mode batch_execute ───────────────────────────────────
|
|
360
|
+
|
|
361
|
+
describe("Context-mode batch_execute", () => {
|
|
362
|
+
it("note with command count", () => {
|
|
363
|
+
const block = toolUse(
|
|
364
|
+
"mcp__plugin_context-mode_context-mode__batch_execute",
|
|
365
|
+
{ commands: [{ cmd: "a" }, { cmd: "b" }, { cmd: "c" }] },
|
|
366
|
+
);
|
|
367
|
+
const result = summarizeToolUse(block);
|
|
368
|
+
assert.ok(result.includes("Context-mode:"));
|
|
369
|
+
assert.ok(result.includes("3 commands"));
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ── 21. Context-mode stats/index ─────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
describe("Context-mode stats/index", () => {
|
|
376
|
+
it("returns null for stats (removed)", () => {
|
|
377
|
+
const block = toolUse("mcp__plugin_context-mode_context-mode__stats", {});
|
|
378
|
+
const result = summarizeToolUse(block);
|
|
379
|
+
assert.equal(result, null);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("returns null for index (removed)", () => {
|
|
383
|
+
const block = toolUse("mcp__plugin_context-mode_context-mode__index", {});
|
|
384
|
+
const result = summarizeToolUse(block);
|
|
385
|
+
assert.equal(result, null);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ── 22. Context7 ────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
describe("Context7", () => {
|
|
392
|
+
it("emits docs note with library name", () => {
|
|
393
|
+
const block = toolUse("mcp__context7__query-docs", {
|
|
394
|
+
libraryName: "react",
|
|
395
|
+
});
|
|
396
|
+
const result = summarizeToolUse(block);
|
|
397
|
+
assert.ok(result.includes("Docs:"));
|
|
398
|
+
assert.ok(result.includes("react"));
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ── 23. Unknown MCP tool ────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
describe("Unknown MCP tool", () => {
|
|
405
|
+
it("preserves name and start+end trimmed input", () => {
|
|
406
|
+
const bigInput = { data: chars(2000) };
|
|
407
|
+
const block = toolUse("mcp__custom__my_tool", bigInput);
|
|
408
|
+
const result = summarizeToolUse(block);
|
|
409
|
+
assert.ok(result.includes("`mcp__custom__my_tool`"));
|
|
410
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("keeps small input in full", () => {
|
|
414
|
+
const block = toolUse("mcp__custom__small_tool", { key: "val" });
|
|
415
|
+
const result = summarizeToolUse(block);
|
|
416
|
+
assert.ok(result.includes("`mcp__custom__small_tool`"));
|
|
417
|
+
assert.ok(result.includes('"key"'));
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ── 24. Unknown built-in tool ───────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
describe("Unknown built-in tool", () => {
|
|
424
|
+
it("same conservative treatment", () => {
|
|
425
|
+
const block = toolUse("SomeFutureTool", { foo: "bar" });
|
|
426
|
+
const result = summarizeToolUse(block);
|
|
427
|
+
assert.ok(result.includes("`SomeFutureTool`"));
|
|
428
|
+
assert.ok(result.includes('"foo"'));
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── 25. Missing/null name ───────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
describe("Missing/null name", () => {
|
|
435
|
+
it("returns generic fallback for null name", () => {
|
|
436
|
+
const block = { type: "tool_use", id: "t1", name: null, input: {} };
|
|
437
|
+
const result = summarizeToolUse(block);
|
|
438
|
+
assert.ok(result.includes("[unknown]"));
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("returns generic fallback for undefined name", () => {
|
|
442
|
+
const block = { type: "tool_use", id: "t1", input: {} };
|
|
443
|
+
const result = summarizeToolUse(block);
|
|
444
|
+
assert.ok(result.includes("[unknown]"));
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// ===========================================================================
|
|
450
|
+
// summarizeToolResult
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
|
|
453
|
+
describe("summarizeToolResult", () => {
|
|
454
|
+
// ── 1. AskUserQuestion — ALWAYS kept ─────────────────────────────────
|
|
455
|
+
|
|
456
|
+
it("keeps AskUserQuestion result in full (user decision)", () => {
|
|
457
|
+
const result = summarizeToolResult(
|
|
458
|
+
toolResult("Yes, please proceed with option B"),
|
|
459
|
+
{ name: "AskUserQuestion" },
|
|
460
|
+
);
|
|
461
|
+
assert.ok(result.includes("User answered:"));
|
|
462
|
+
assert.ok(result.includes("Yes, please proceed with option B"));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ── 2. Read result — removed ─────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
it("removes Read result (re-obtainable)", () => {
|
|
468
|
+
const longContent = chars(5000);
|
|
469
|
+
const result = summarizeToolResult(toolResult(longContent), {
|
|
470
|
+
name: "Read",
|
|
471
|
+
});
|
|
472
|
+
assert.equal(result, null);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ── 3. Read result with short error ──────────────────────────────────
|
|
476
|
+
|
|
477
|
+
it("keeps short error from Read result (<500 chars)", () => {
|
|
478
|
+
const result = summarizeToolResult(toolResult("Error: file not found"), {
|
|
479
|
+
name: "Read",
|
|
480
|
+
});
|
|
481
|
+
assert.ok(result !== null);
|
|
482
|
+
assert.ok(result.includes("Error"));
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ── 4. Read result with long content containing 'error' ──────────────
|
|
486
|
+
|
|
487
|
+
it("removes Read result with long content containing 'error' (false positive)", () => {
|
|
488
|
+
// A long file that happens to mention "error" is not a tool failure
|
|
489
|
+
const longContent = `${chars(600)} error ${chars(600)}`;
|
|
490
|
+
const result = summarizeToolResult(toolResult(longContent), {
|
|
491
|
+
name: "Read",
|
|
492
|
+
});
|
|
493
|
+
assert.equal(result, null);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ── 5. Grep/Glob result — removed ────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
it("removes Grep result", () => {
|
|
499
|
+
const result = summarizeToolResult(toolResult("src/a.js:10: match"), {
|
|
500
|
+
name: "Grep",
|
|
501
|
+
});
|
|
502
|
+
assert.equal(result, null);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("removes Glob result", () => {
|
|
506
|
+
const result = summarizeToolResult(toolResult("src/a.js\nsrc/b.js"), {
|
|
507
|
+
name: "Glob",
|
|
508
|
+
});
|
|
509
|
+
assert.equal(result, null);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ── 6. Edit/Write result — removed ───────────────────────────────────
|
|
513
|
+
|
|
514
|
+
it("removes Edit result", () => {
|
|
515
|
+
const result = summarizeToolResult(toolResult("File edited successfully"), {
|
|
516
|
+
name: "Edit",
|
|
517
|
+
});
|
|
518
|
+
assert.equal(result, null);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("removes Write result", () => {
|
|
522
|
+
const result = summarizeToolResult(
|
|
523
|
+
toolResult("File written successfully"),
|
|
524
|
+
{ name: "Write" },
|
|
525
|
+
);
|
|
526
|
+
assert.equal(result, null);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// ── 7. Bash result, short (<5000) ────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
it("keeps short Bash result in full", () => {
|
|
532
|
+
const result = summarizeToolResult(toolResult("12 passed, 0 failed"), {
|
|
533
|
+
name: "Bash",
|
|
534
|
+
});
|
|
535
|
+
assert.ok(result !== null);
|
|
536
|
+
assert.ok(result.includes("12 passed, 0 failed"));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// ── 8. Bash result, long (>5000) ─────────────────────────────────────
|
|
540
|
+
|
|
541
|
+
it("trims long Bash result with start+end", () => {
|
|
542
|
+
const longOutput = chars(8000, "o");
|
|
543
|
+
const result = summarizeToolResult(toolResult(longOutput), {
|
|
544
|
+
name: "Bash",
|
|
545
|
+
});
|
|
546
|
+
assert.ok(result !== null);
|
|
547
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
548
|
+
assert.ok(result.length < longOutput.length);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// ── 9. Agent result, short ───────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
it("keeps short Agent result in full", () => {
|
|
554
|
+
const result = summarizeToolResult(
|
|
555
|
+
toolResult("Found 3 usages of deprecated API"),
|
|
556
|
+
{ name: "Agent" },
|
|
557
|
+
);
|
|
558
|
+
assert.ok(result !== null);
|
|
559
|
+
assert.ok(result.includes("Agent result:"));
|
|
560
|
+
assert.ok(result.includes("Found 3 usages"));
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ── 10. Agent result, long (>2000) ───────────────────────────────────
|
|
564
|
+
|
|
565
|
+
it("trims long Agent result with start+end", () => {
|
|
566
|
+
const longResult = chars(3000);
|
|
567
|
+
const result = summarizeToolResult(toolResult(longResult), {
|
|
568
|
+
name: "Agent",
|
|
569
|
+
});
|
|
570
|
+
assert.ok(result !== null);
|
|
571
|
+
assert.ok(result.includes("Agent result:"));
|
|
572
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ── 11. Sequential thinking result — removed ─────────────────────────
|
|
576
|
+
|
|
577
|
+
it("removes sequential thinking result (redundant)", () => {
|
|
578
|
+
const result = summarizeToolResult(toolResult("Thought recorded"), {
|
|
579
|
+
name: "mcp__sequential-thinking__sequentialthinking",
|
|
580
|
+
});
|
|
581
|
+
assert.equal(result, null);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ── 12. Context-mode result — removed ────────────────────────────────
|
|
585
|
+
|
|
586
|
+
it("removes context-mode result", () => {
|
|
587
|
+
const result = summarizeToolResult(toolResult("Execution complete"), {
|
|
588
|
+
name: "mcp__plugin_context-mode_context-mode__execute",
|
|
589
|
+
});
|
|
590
|
+
assert.equal(result, null);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// ── 13. Serena memory result — removed ───────────────────────────────
|
|
594
|
+
|
|
595
|
+
it("removes Serena memory result", () => {
|
|
596
|
+
const result = summarizeToolResult(toolResult("Memory saved"), {
|
|
597
|
+
name: "mcp__serena__write_memory",
|
|
598
|
+
});
|
|
599
|
+
assert.equal(result, null);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ── 14. Non-re-obtainable tool with error content ────────────────────
|
|
603
|
+
|
|
604
|
+
it("keeps error from non-re-obtainable tool", () => {
|
|
605
|
+
const result = summarizeToolResult(
|
|
606
|
+
toolResult("Error: connection timeout"),
|
|
607
|
+
{ name: "SomeCustomTool" },
|
|
608
|
+
);
|
|
609
|
+
assert.ok(result !== null);
|
|
610
|
+
assert.ok(result.includes("Error"));
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// ── 15. Unknown tool, short result (<1000) ──────────────────────────
|
|
614
|
+
|
|
615
|
+
it("keeps short unknown tool result", () => {
|
|
616
|
+
const result = summarizeToolResult(toolResult("some short output"), {
|
|
617
|
+
name: "UnknownTool",
|
|
618
|
+
});
|
|
619
|
+
assert.ok(result !== null);
|
|
620
|
+
assert.ok(result.includes("some short output"));
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// ── 16. Unknown tool, long result (>=1000) ──────────────────────────
|
|
624
|
+
|
|
625
|
+
it("trims long unknown tool result", () => {
|
|
626
|
+
const longResult = chars(1500);
|
|
627
|
+
const result = summarizeToolResult(toolResult(longResult), {
|
|
628
|
+
name: "UnknownTool",
|
|
629
|
+
});
|
|
630
|
+
assert.ok(result !== null);
|
|
631
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// ── 17. Null/empty result content ───────────────────────────────────
|
|
635
|
+
|
|
636
|
+
it("returns null for null result block", () => {
|
|
637
|
+
const result = summarizeToolResult(null, { name: "Bash" });
|
|
638
|
+
assert.equal(result, null);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("returns null for empty string content", () => {
|
|
642
|
+
const result = summarizeToolResult(toolResult(""), { name: "Bash" });
|
|
643
|
+
assert.equal(result, null);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("returns null for missing content field", () => {
|
|
647
|
+
const result = summarizeToolResult(
|
|
648
|
+
{ type: "tool_result", tool_use_id: "t1" },
|
|
649
|
+
{ name: "Bash" },
|
|
650
|
+
);
|
|
651
|
+
assert.equal(result, null);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// ── Array content format ────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
it("handles array content format in tool_result", () => {
|
|
657
|
+
const result = summarizeToolResult(toolResultArray(["line 1", "line 2"]), {
|
|
658
|
+
name: "Bash",
|
|
659
|
+
});
|
|
660
|
+
assert.ok(result !== null);
|
|
661
|
+
assert.ok(result.includes("line 1"));
|
|
662
|
+
assert.ok(result.includes("line 2"));
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ===========================================================================
|
|
667
|
+
// formatEditDiff
|
|
668
|
+
// ===========================================================================
|
|
669
|
+
|
|
670
|
+
describe("formatEditDiff", () => {
|
|
671
|
+
it("shows both old and new for small edit", () => {
|
|
672
|
+
const result = formatEditDiff("app.js", "const a = 1;", "const a = 2;");
|
|
673
|
+
assert.ok(result.includes("Edit `app.js`"));
|
|
674
|
+
assert.ok(result.includes("old:"));
|
|
675
|
+
assert.ok(result.includes("new:"));
|
|
676
|
+
assert.ok(result.includes("const a = 1;"));
|
|
677
|
+
assert.ok(result.includes("const a = 2;"));
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("trims each side independently for large edit", () => {
|
|
681
|
+
const bigOld = chars(3000, "a");
|
|
682
|
+
const bigNew = chars(3000, "b");
|
|
683
|
+
const result = formatEditDiff("big.js", bigOld, bigNew);
|
|
684
|
+
assert.ok(result.includes("Edit `big.js`"));
|
|
685
|
+
assert.ok(result.includes("old:"));
|
|
686
|
+
assert.ok(result.includes("new:"));
|
|
687
|
+
assert.ok(result.includes("trimmed from middle"));
|
|
688
|
+
// Both sides should be trimmed — total should be well under 6000
|
|
689
|
+
assert.ok(result.length < bigOld.length + bigNew.length);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("shows only new: block for pure insertion (no old_string)", () => {
|
|
693
|
+
const result = formatEditDiff("app.js", "", "const b = 2;");
|
|
694
|
+
assert.ok(result.includes("new:"));
|
|
695
|
+
assert.ok(!result.includes("old:"));
|
|
696
|
+
assert.ok(result.includes("const b = 2;"));
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("shows old: block with [deleted] note for pure deletion", () => {
|
|
700
|
+
const result = formatEditDiff("app.js", "const c = 3;", "");
|
|
701
|
+
assert.ok(result.includes("old:"));
|
|
702
|
+
assert.ok(result.includes("[deleted]"));
|
|
703
|
+
assert.ok(!result.includes("new:"));
|
|
704
|
+
assert.ok(result.includes("const c = 3;"));
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("still has Edit header when both are empty", () => {
|
|
708
|
+
const result = formatEditDiff("app.js", "", "");
|
|
709
|
+
assert.ok(result.includes("Edit `app.js`"));
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// ===========================================================================
|
|
714
|
+
// contentBlockPlaceholder
|
|
715
|
+
// ===========================================================================
|
|
716
|
+
|
|
717
|
+
describe("contentBlockPlaceholder", () => {
|
|
718
|
+
it("returns image placeholder for image block", () => {
|
|
719
|
+
const result = contentBlockPlaceholder({ type: "image" });
|
|
720
|
+
assert.equal(result, "[User shared an image]");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("returns document placeholder with filename", () => {
|
|
724
|
+
const result = contentBlockPlaceholder({
|
|
725
|
+
type: "document",
|
|
726
|
+
source: { filename: "report.pdf" },
|
|
727
|
+
});
|
|
728
|
+
assert.equal(result, "[User shared a document: report.pdf]");
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("returns document placeholder without filename", () => {
|
|
732
|
+
const result = contentBlockPlaceholder({ type: "document" });
|
|
733
|
+
assert.equal(result, "[User shared a document]");
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("returns null for text block", () => {
|
|
737
|
+
assert.equal(contentBlockPlaceholder({ type: "text", text: "hi" }), null);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("returns null for tool_use block", () => {
|
|
741
|
+
assert.equal(
|
|
742
|
+
contentBlockPlaceholder({ type: "tool_use", name: "Edit" }),
|
|
743
|
+
null,
|
|
744
|
+
);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("returns null for tool_result block", () => {
|
|
748
|
+
assert.equal(
|
|
749
|
+
contentBlockPlaceholder({ type: "tool_result", content: "ok" }),
|
|
750
|
+
null,
|
|
751
|
+
);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("returns null for thinking block", () => {
|
|
755
|
+
assert.equal(
|
|
756
|
+
contentBlockPlaceholder({ type: "thinking", thinking: "hmm" }),
|
|
757
|
+
null,
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("returns unknown placeholder for unrecognised type", () => {
|
|
762
|
+
const result = contentBlockPlaceholder({ type: "foo" });
|
|
763
|
+
assert.equal(result, "[Unknown content block: foo]");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("returns null for null/missing block", () => {
|
|
767
|
+
assert.equal(contentBlockPlaceholder(null), null);
|
|
768
|
+
assert.equal(contentBlockPlaceholder(undefined), null);
|
|
769
|
+
assert.equal(contentBlockPlaceholder({}), null);
|
|
770
|
+
});
|
|
771
|
+
});
|