squeezr-ai 1.46.2 → 1.80.6
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 +189 -315
- package/bin/squeezr.js +2535 -2251
- package/dist/__tests__/aiRateLimit.test.d.ts +1 -0
- package/dist/__tests__/aiRateLimit.test.js +20 -0
- package/dist/__tests__/attachmentDedup.test.d.ts +1 -0
- package/dist/__tests__/attachmentDedup.test.js +89 -0
- package/dist/__tests__/compressibilityProbe.test.d.ts +1 -0
- package/dist/__tests__/compressibilityProbe.test.js +45 -0
- package/dist/__tests__/compressionGuard.test.d.ts +1 -0
- package/dist/__tests__/compressionGuard.test.js +57 -0
- package/dist/__tests__/compressor.test.js +104 -51
- package/dist/__tests__/diffRead.test.d.ts +1 -0
- package/dist/__tests__/diffRead.test.js +83 -0
- package/dist/__tests__/glossaryStore.test.d.ts +1 -0
- package/dist/__tests__/glossaryStore.test.js +37 -0
- package/dist/__tests__/glossarySub.test.d.ts +1 -0
- package/dist/__tests__/glossarySub.test.js +162 -0
- package/dist/__tests__/imageDedup.test.d.ts +1 -0
- package/dist/__tests__/imageDedup.test.js +80 -0
- package/dist/__tests__/largeBlock.test.d.ts +1 -0
- package/dist/__tests__/largeBlock.test.js +35 -0
- package/dist/__tests__/mcpFilter.test.d.ts +1 -0
- package/dist/__tests__/mcpFilter.test.js +87 -0
- package/dist/__tests__/newFeatures.test.d.ts +1 -0
- package/dist/__tests__/newFeatures.test.js +124 -0
- package/dist/__tests__/qualityHarness.test.d.ts +1 -0
- package/dist/__tests__/qualityHarness.test.js +98 -0
- package/dist/__tests__/rateLimitHeaders.test.js +6 -0
- package/dist/__tests__/requestCapture.test.d.ts +1 -0
- package/dist/__tests__/requestCapture.test.js +37 -0
- package/dist/__tests__/skillDedup.test.d.ts +1 -0
- package/dist/__tests__/skillDedup.test.js +57 -0
- package/dist/__tests__/staleTurns.test.d.ts +1 -0
- package/dist/__tests__/staleTurns.test.js +113 -0
- package/dist/__tests__/structuredGuard.test.d.ts +1 -0
- package/dist/__tests__/structuredGuard.test.js +72 -0
- package/dist/__tests__/toolDescComp.test.d.ts +1 -0
- package/dist/__tests__/toolDescComp.test.js +157 -0
- package/dist/__tests__/toolResultDedup.test.d.ts +1 -0
- package/dist/__tests__/toolResultDedup.test.js +40 -0
- package/dist/aiRateLimit.d.ts +19 -0
- package/dist/aiRateLimit.js +35 -0
- package/dist/aiToggle.d.ts +14 -0
- package/dist/aiToggle.js +53 -0
- package/dist/attachmentCompress.d.ts +9 -0
- package/dist/attachmentCompress.js +211 -0
- package/dist/attachmentDedup.d.ts +9 -0
- package/dist/attachmentDedup.js +89 -0
- package/dist/bypass.d.ts +6 -3
- package/dist/bypass.js +37 -5
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +10 -0
- package/dist/circuitBreaker.d.ts +4 -2
- package/dist/circuitBreaker.js +6 -3
- package/dist/compressibilityProbe.d.ts +8 -0
- package/dist/compressibilityProbe.js +47 -0
- package/dist/compressionGuard.d.ts +31 -0
- package/dist/compressionGuard.js +101 -0
- package/dist/compressor.d.ts +51 -1
- package/dist/compressor.js +599 -73
- package/dist/config.d.ts +21 -1
- package/dist/config.js +58 -2
- package/dist/dashboard.d.ts +3 -1
- package/dist/dashboard.js +2163 -1655
- package/dist/diffRead.d.ts +9 -0
- package/dist/diffRead.js +149 -0
- package/dist/expand.d.ts +2 -0
- package/dist/expand.js +6 -0
- package/dist/glossaryStore.d.ts +28 -0
- package/dist/glossaryStore.js +131 -0
- package/dist/glossarySub.d.ts +38 -0
- package/dist/glossarySub.js +123 -0
- package/dist/history.d.ts +35 -1
- package/dist/history.js +31 -5
- package/dist/identGlossary.d.ts +20 -0
- package/dist/identGlossary.js +215 -0
- package/dist/imageDedup.d.ts +12 -0
- package/dist/imageDedup.js +98 -0
- package/dist/index.js +7 -0
- package/dist/limits.d.ts +5 -2
- package/dist/limits.js +47 -4
- package/dist/logFeed.d.ts +10 -0
- package/dist/logFeed.js +42 -0
- package/dist/mcpFilter.d.ts +43 -0
- package/dist/mcpFilter.js +89 -0
- package/dist/mcpToolFilter.d.ts +32 -0
- package/dist/mcpToolFilter.js +140 -0
- package/dist/probePort.js +5 -1
- package/dist/promptCache.d.ts +44 -0
- package/dist/promptCache.js +121 -0
- package/dist/qualityGovernor.d.ts +11 -0
- package/dist/qualityGovernor.js +69 -0
- package/dist/requestCapture.d.ts +21 -0
- package/dist/requestCapture.js +79 -0
- package/dist/semanticRead.d.ts +9 -0
- package/dist/semanticRead.js +188 -0
- package/dist/server.js +1398 -992
- package/dist/sessionCache.js +9 -2
- package/dist/skillDedup.d.ts +5 -0
- package/dist/skillDedup.js +89 -0
- package/dist/staleTurnSummary.d.ts +9 -0
- package/dist/staleTurnSummary.js +110 -0
- package/dist/staleTurns.d.ts +14 -0
- package/dist/staleTurns.js +80 -0
- package/dist/stats.d.ts +16 -3
- package/dist/stats.js +157 -21
- package/dist/stockToolDescs.d.ts +12 -0
- package/dist/stockToolDescs.js +69 -0
- package/dist/structuredGuard.d.ts +25 -0
- package/dist/structuredGuard.js +116 -0
- package/dist/systemPrompt.js +6 -2
- package/dist/systemSectioning.d.ts +21 -0
- package/dist/systemSectioning.js +111 -0
- package/dist/toolDescComp.d.ts +30 -0
- package/dist/toolDescComp.js +81 -0
- package/dist/toolResultDedup.d.ts +9 -0
- package/dist/toolResultDedup.js +88 -0
- package/package.json +69 -66
- package/squeezr.toml +18 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { tryConsumeAiCall, aiCallsRemaining, _config } from '../aiRateLimit.js';
|
|
3
|
+
describe('aiRateLimit', () => {
|
|
4
|
+
it('allows up to MAX_CALLS_PER_WINDOW calls then blocks', () => {
|
|
5
|
+
// Fresh module state — consume the whole window
|
|
6
|
+
let allowed = 0;
|
|
7
|
+
for (let i = 0; i < _config.MAX_CALLS_PER_WINDOW + 10; i++) {
|
|
8
|
+
if (tryConsumeAiCall())
|
|
9
|
+
allowed++;
|
|
10
|
+
}
|
|
11
|
+
expect(allowed).toBe(_config.MAX_CALLS_PER_WINDOW);
|
|
12
|
+
});
|
|
13
|
+
it('reports 0 remaining once exhausted', () => {
|
|
14
|
+
// Window already exhausted by the previous test (same module instance)
|
|
15
|
+
expect(aiCallsRemaining()).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
it('blocks further calls after exhaustion', () => {
|
|
18
|
+
expect(tryConsumeAiCall()).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for v1.49.0 attachment/artifact dedup.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { dedupAttachments } from '../attachmentDedup.js';
|
|
6
|
+
describe('attachmentDedup', () => {
|
|
7
|
+
it('dedupes large repeated text blocks', () => {
|
|
8
|
+
const big = 'X'.repeat(800);
|
|
9
|
+
const messages = [
|
|
10
|
+
{ role: 'user', content: [{ type: 'text', text: big }] },
|
|
11
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
|
|
12
|
+
{ role: 'user', content: [{ type: 'text', text: big }] },
|
|
13
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'live answer' }] },
|
|
14
|
+
{ role: 'user', content: [{ type: 'text', text: 'live ask' }] },
|
|
15
|
+
];
|
|
16
|
+
const r = dedupAttachments(messages);
|
|
17
|
+
expect(r.dedupCount).toBeGreaterThanOrEqual(1);
|
|
18
|
+
expect(r.savedChars).toBeGreaterThan(0);
|
|
19
|
+
});
|
|
20
|
+
it('never touches the last user message (live ask)', () => {
|
|
21
|
+
const big = 'Y'.repeat(800);
|
|
22
|
+
const messages = [
|
|
23
|
+
{ role: 'user', content: [{ type: 'text', text: big }] },
|
|
24
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
|
|
25
|
+
{ role: 'user', content: [{ type: 'text', text: big }] }, // LAST user — should be preserved
|
|
26
|
+
];
|
|
27
|
+
dedupAttachments(messages);
|
|
28
|
+
expect(messages[2].content[0].text).toBe(big);
|
|
29
|
+
});
|
|
30
|
+
it('never touches the last assistant message (live answer)', () => {
|
|
31
|
+
const big = 'Z'.repeat(800);
|
|
32
|
+
const messages = [
|
|
33
|
+
{ role: 'assistant', content: [{ type: 'text', text: big }] },
|
|
34
|
+
{ role: 'user', content: [{ type: 'text', text: 'x' }] },
|
|
35
|
+
{ role: 'assistant', content: [{ type: 'text', text: big }] }, // LAST assistant — preserved
|
|
36
|
+
{ role: 'user', content: [{ type: 'text', text: 'follow up' }] },
|
|
37
|
+
];
|
|
38
|
+
dedupAttachments(messages);
|
|
39
|
+
expect(messages[2].content[0].text).toBe(big);
|
|
40
|
+
});
|
|
41
|
+
it('does not replace blocks below MIN_BLOCK_CHARS threshold', () => {
|
|
42
|
+
const small = 'hi';
|
|
43
|
+
const messages = [
|
|
44
|
+
{ role: 'user', content: [{ type: 'text', text: small }] },
|
|
45
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'ack' }] },
|
|
46
|
+
{ role: 'user', content: [{ type: 'text', text: small }] },
|
|
47
|
+
{ role: 'user', content: [{ type: 'text', text: 'live' }] },
|
|
48
|
+
];
|
|
49
|
+
const r = dedupAttachments(messages);
|
|
50
|
+
expect(r.dedupCount).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
it('never touches tool_use or tool_result blocks', () => {
|
|
53
|
+
const big = 'W'.repeat(800);
|
|
54
|
+
const messages = [
|
|
55
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/x' } }] },
|
|
56
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: big }] },
|
|
57
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/x' } }] },
|
|
58
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't2', content: big }] },
|
|
59
|
+
{ role: 'user', content: [{ type: 'text', text: 'live ask' }] },
|
|
60
|
+
];
|
|
61
|
+
dedupAttachments(messages);
|
|
62
|
+
expect(messages[1].content[0].type).toBe('tool_result');
|
|
63
|
+
expect(messages[3].content[0].type).toBe('tool_result');
|
|
64
|
+
expect(messages[1].content[0].content).toBe(big);
|
|
65
|
+
});
|
|
66
|
+
it('keeps single-occurrence text blocks untouched', () => {
|
|
67
|
+
const messages = [
|
|
68
|
+
{ role: 'user', content: [{ type: 'text', text: 'A'.repeat(800) }] },
|
|
69
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'B'.repeat(800) }] },
|
|
70
|
+
{ role: 'user', content: [{ type: 'text', text: 'live' }] },
|
|
71
|
+
];
|
|
72
|
+
const r = dedupAttachments(messages);
|
|
73
|
+
expect(r.dedupCount).toBe(0);
|
|
74
|
+
expect(messages[0].content[0].text).toBe('A'.repeat(800));
|
|
75
|
+
expect(messages[1].content[0].text).toBe('B'.repeat(800));
|
|
76
|
+
});
|
|
77
|
+
it('produces non-empty placeholder text (never empty content)', () => {
|
|
78
|
+
const big = 'D'.repeat(800);
|
|
79
|
+
const messages = [
|
|
80
|
+
{ role: 'user', content: [{ type: 'text', text: big }] },
|
|
81
|
+
{ role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
|
|
82
|
+
{ role: 'user', content: [{ type: 'text', text: big }] },
|
|
83
|
+
{ role: 'user', content: [{ type: 'text', text: 'live' }] },
|
|
84
|
+
];
|
|
85
|
+
dedupAttachments(messages);
|
|
86
|
+
expect(typeof messages[0].content[0].text).toBe('string');
|
|
87
|
+
expect(messages[0].content[0].text.length).toBeGreaterThan(20);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { looksIncompressible, deflateRatio } from '../compressibilityProbe.js';
|
|
3
|
+
// Calibrated against real Zest output (see compressibilityProbe.ts):
|
|
4
|
+
// verbose log w/ repeated lines → deflate ~0.17 → Zest saved 56% → KEEP
|
|
5
|
+
// dense error/path list → deflate ~0.76 → Zest saved 0% → SKIP
|
|
6
|
+
// test output (mixed) → deflate ~0.63 → Zest saved 5% → SKIP
|
|
7
|
+
const VERBOSE = ('npm warn deprecated foo@1.0.0: use bar\n' +
|
|
8
|
+
'npm warn deprecated foo@1.0.0: use bar\n' +
|
|
9
|
+
'npm warn deprecated baz@2.0.0: no longer maintained\n' +
|
|
10
|
+
'added 1242 packages, and audited 1243 packages in 14s\n' +
|
|
11
|
+
'201 packages are looking for funding\n' +
|
|
12
|
+
' run `npm fund` for details\n' +
|
|
13
|
+
'found 0 vulnerabilities\n' +
|
|
14
|
+
'gardening node_modules ... done\n' +
|
|
15
|
+
'gardening node_modules ... done\n' +
|
|
16
|
+
'gardening node_modules ... done').repeat(3);
|
|
17
|
+
const DENSE_PATHS = ('src/compressor.ts:268 error TS2322: Type string is not assignable\n' +
|
|
18
|
+
'src/dashboard.ts:1027 warning unused var aiSavedTok\n' +
|
|
19
|
+
'src/server.ts:956 note: see https://docs.foo.com/E1234 for details\n' +
|
|
20
|
+
'src/stats.ts:361 error ENOENT no such file\n' +
|
|
21
|
+
'src/cache.ts:42 error TS2304: cannot find name foo\n' +
|
|
22
|
+
'src/expand.ts:88 warning deprecated symbol bar used here\n' +
|
|
23
|
+
'src/index.ts:12 error TS1005: semicolon expected near token');
|
|
24
|
+
describe('looksIncompressible', () => {
|
|
25
|
+
it('keeps redundant/verbose blocks (AI will compress them well)', () => {
|
|
26
|
+
expect(looksIncompressible(VERBOSE)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('skips dense path/error dumps (AI would be rejected — wasted call)', () => {
|
|
29
|
+
expect(looksIncompressible(DENSE_PATHS)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
it('does not probe tiny blocks (deflate ratio is noisy there)', () => {
|
|
32
|
+
expect(looksIncompressible('error: ENOENT')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('respects a custom maxDeflate threshold', () => {
|
|
35
|
+
// VERBOSE deflates to ~0.17; a 0.1 threshold flips it to "incompressible".
|
|
36
|
+
expect(looksIncompressible(VERBOSE, 0.1)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('deflateRatio is in (0,1] and lower for redundant text', () => {
|
|
39
|
+
const rVerbose = deflateRatio(VERBOSE);
|
|
40
|
+
const rDense = deflateRatio(DENSE_PATHS);
|
|
41
|
+
expect(rVerbose).toBeGreaterThan(0);
|
|
42
|
+
expect(rVerbose).toBeLessThan(rDense);
|
|
43
|
+
expect(rDense).toBeLessThanOrEqual(1);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateCompression } from '../compressionGuard.js';
|
|
3
|
+
describe('validateCompression', () => {
|
|
4
|
+
it('accepts a good compression that keeps key tokens and saves enough', () => {
|
|
5
|
+
const original = 'Running tests in src/auth/login.ts\n' +
|
|
6
|
+
'PASS 42 tests, FAIL 1: expected 200 got 401 at line 88\n' +
|
|
7
|
+
'See https://example.com/docs/errors for details\n' +
|
|
8
|
+
'lots of incidental filler text repeated over and over to pad the block length so the ratio is meaningful '.repeat(8);
|
|
9
|
+
const compressed = 'FAIL src/auth/login.ts line 88: expected 200 got 401. See https://example.com/docs/errors';
|
|
10
|
+
const r = validateCompression(original, compressed);
|
|
11
|
+
expect(r.accept).toBe(true);
|
|
12
|
+
expect(r.ratio).toBeGreaterThan(0.15);
|
|
13
|
+
});
|
|
14
|
+
it('rejects when the result is LONGER than the original (negative savings)', () => {
|
|
15
|
+
const original = 'short output';
|
|
16
|
+
const compressed = 'this compressed result is actually much longer than the original input text';
|
|
17
|
+
const r = validateCompression(original, compressed);
|
|
18
|
+
expect(r.accept).toBe(false);
|
|
19
|
+
expect(r.reason).toMatch(/ratio/);
|
|
20
|
+
});
|
|
21
|
+
it('rejects empty output', () => {
|
|
22
|
+
const r = validateCompression('some real content '.repeat(20), ' ');
|
|
23
|
+
expect(r.accept).toBe(false);
|
|
24
|
+
expect(r.reason).toBe('empty output');
|
|
25
|
+
});
|
|
26
|
+
it('rejects when a critical file path is dropped', () => {
|
|
27
|
+
const original = 'Error in src/payments/checkout.ts at line 12\n' +
|
|
28
|
+
'stack trace filler '.repeat(40);
|
|
29
|
+
const compressed = 'Error at line 12'; // dropped the path
|
|
30
|
+
const r = validateCompression(original, compressed);
|
|
31
|
+
expect(r.accept).toBe(false);
|
|
32
|
+
expect(r.reason).toMatch(/critical token/);
|
|
33
|
+
});
|
|
34
|
+
it('rejects when an error code is dropped', () => {
|
|
35
|
+
const original = 'connection failed ECONNREFUSED on attempt 3\n' + 'retry filler '.repeat(40);
|
|
36
|
+
const compressed = 'connection failed on attempt 3'; // dropped ECONNREFUSED
|
|
37
|
+
const r = validateCompression(original, compressed);
|
|
38
|
+
expect(r.accept).toBe(false);
|
|
39
|
+
expect(r.reason).toMatch(/critical token/);
|
|
40
|
+
});
|
|
41
|
+
it('rejects when an URL is dropped', () => {
|
|
42
|
+
const original = 'fetch https://api.service.com/v2/users returned 500\n' + 'body filler '.repeat(40);
|
|
43
|
+
const compressed = 'fetch returned 500'; // dropped URL (and 500 is HTTP code, also hard)
|
|
44
|
+
const r = validateCompression(original, compressed);
|
|
45
|
+
expect(r.accept).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('tolerates dropping a few incidental soft tokens', () => {
|
|
48
|
+
const original = 'function computeTotals iterates items and calls helperOne helperTwo helperThree\n' +
|
|
49
|
+
'verbose explanation filler text '.repeat(30);
|
|
50
|
+
// keeps the main identifier, drops a couple of minor helpers — within tolerance
|
|
51
|
+
const compressed = 'computeTotals iterates items, calls helpers';
|
|
52
|
+
const r = validateCompression(original, compressed, { minRatio: 0.1 });
|
|
53
|
+
// soft tolerance is small, so this MAY reject; assert the function runs and returns a ratio
|
|
54
|
+
expect(typeof r.accept).toBe('boolean');
|
|
55
|
+
expect(r.ratio).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -3,24 +3,36 @@ import { clearExpandStore } from '../expand.js';
|
|
|
3
3
|
import { clearSessionCache } from '../sessionCache.js';
|
|
4
4
|
// Mock AI SDKs before importing compressor
|
|
5
5
|
vi.mock('@anthropic-ai/sdk', () => ({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}),
|
|
11
|
-
},
|
|
12
|
-
})),
|
|
13
|
-
}));
|
|
14
|
-
vi.mock('openai', () => ({
|
|
15
|
-
default: vi.fn().mockImplementation(() => ({
|
|
16
|
-
chat: {
|
|
17
|
-
completions: {
|
|
6
|
+
// function (not arrow) — `new Anthropic()` requires a constructable implementation
|
|
7
|
+
default: vi.fn().mockImplementation(function () {
|
|
8
|
+
return {
|
|
9
|
+
messages: {
|
|
18
10
|
create: vi.fn().mockResolvedValue({
|
|
19
|
-
|
|
11
|
+
content: [{ text: 'AI compressed summary' }],
|
|
20
12
|
}),
|
|
21
13
|
},
|
|
22
|
-
}
|
|
23
|
-
})
|
|
14
|
+
};
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('openai', () => ({
|
|
18
|
+
default: vi.fn().mockImplementation(function () {
|
|
19
|
+
return {
|
|
20
|
+
chat: {
|
|
21
|
+
completions: {
|
|
22
|
+
create: vi.fn().mockResolvedValue({
|
|
23
|
+
choices: [{ message: { content: 'AI compressed summary' } }],
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
// Force the AI compression master toggle ON for these tests (production default
|
|
31
|
+
// is off + persisted to disk; tests must not depend on the user's local state).
|
|
32
|
+
vi.mock('../aiToggle.js', () => ({
|
|
33
|
+
isAiCompressionEnabled: () => true,
|
|
34
|
+
setAiCompression: () => { },
|
|
35
|
+
toggleAiCompression: () => true,
|
|
24
36
|
}));
|
|
25
37
|
// Mock fetch for Gemini
|
|
26
38
|
const mockFetch = vi.fn().mockResolvedValue({
|
|
@@ -50,6 +62,11 @@ const baseConfig = {
|
|
|
50
62
|
shouldSkipTool: () => false,
|
|
51
63
|
skipTools: new Set(),
|
|
52
64
|
onlyTools: new Set(),
|
|
65
|
+
aiSkipTools: new Set(),
|
|
66
|
+
aiCompression: true, // tests exercise the AI path; production default is false
|
|
67
|
+
compressConversation: false,
|
|
68
|
+
keepRecentAssistant: 3,
|
|
69
|
+
assistantThreshold: 300,
|
|
53
70
|
};
|
|
54
71
|
beforeEach(() => {
|
|
55
72
|
clearExpandStore();
|
|
@@ -89,9 +106,8 @@ describe('compressAnthropicMessages', () => {
|
|
|
89
106
|
expect(block.content).not.toContain('[squeezr:');
|
|
90
107
|
});
|
|
91
108
|
it('compresses old blocks beyond keepRecent', async () => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const msgs = makeMessages([longText, longText]);
|
|
109
|
+
// distinct texts — identical blocks would be collapsed by cross-turn dedup first
|
|
110
|
+
const msgs = makeMessages(['x'.repeat(1600), 'y'.repeat(1600)]);
|
|
95
111
|
const [result, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
96
112
|
// First block should be compressed
|
|
97
113
|
const firstBlock = result[1].content[0];
|
|
@@ -99,8 +115,7 @@ describe('compressAnthropicMessages', () => {
|
|
|
99
115
|
expect(savings.compressed).toBe(1);
|
|
100
116
|
});
|
|
101
117
|
it('embeds squeezr ID and ratio in compressed content', async () => {
|
|
102
|
-
const
|
|
103
|
-
const msgs = makeMessages([longText, longText]);
|
|
118
|
+
const msgs = makeMessages(['x'.repeat(1600), 'y'.repeat(1600)]);
|
|
104
119
|
const [result] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
105
120
|
const compressed = result[1].content[0].content;
|
|
106
121
|
expect(compressed).toMatch(/\[squeezr:[a-f0-9]{6} -\d+%\]/);
|
|
@@ -112,8 +127,7 @@ describe('compressAnthropicMessages', () => {
|
|
|
112
127
|
expect(savings.compressed).toBe(0);
|
|
113
128
|
});
|
|
114
129
|
it('returns dry-run savings without modifying messages', async () => {
|
|
115
|
-
const
|
|
116
|
-
const msgs = makeMessages([longText, longText]);
|
|
130
|
+
const msgs = makeMessages(['x'.repeat(1600), 'y'.repeat(1600)]);
|
|
117
131
|
const [result, savings] = await compressAnthropicMessages(msgs, 'key', { ...baseConfig, dryRun: true });
|
|
118
132
|
expect(savings.dryRun).toBe(true);
|
|
119
133
|
// Messages should not be modified
|
|
@@ -122,8 +136,7 @@ describe('compressAnthropicMessages', () => {
|
|
|
122
136
|
});
|
|
123
137
|
it('uses session cache on second call with same content', async () => {
|
|
124
138
|
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
125
|
-
const
|
|
126
|
-
const msgs = makeMessages([longText, longText]);
|
|
139
|
+
const msgs = makeMessages(['x'.repeat(200), 'y'.repeat(200)]);
|
|
127
140
|
// First call — compresses
|
|
128
141
|
await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
129
142
|
const callsAfterFirst = Anthropic.mock.results[0]?.value?.messages?.create?.mock?.calls?.length ?? 0;
|
|
@@ -143,13 +156,56 @@ describe('compressAnthropicMessages', () => {
|
|
|
143
156
|
expect(content).not.toContain('context4');
|
|
144
157
|
});
|
|
145
158
|
it('tracks savings correctly', async () => {
|
|
146
|
-
const
|
|
147
|
-
const msgs = makeMessages([longText, longText]);
|
|
159
|
+
const msgs = makeMessages(['x'.repeat(1600), 'y'.repeat(1600)]);
|
|
148
160
|
const [, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
149
161
|
expect(savings.savedChars).toBeGreaterThan(0);
|
|
150
162
|
expect(savings.originalChars).toBeGreaterThan(0);
|
|
151
163
|
expect(savings.byTool.length).toBeGreaterThan(0);
|
|
152
164
|
});
|
|
165
|
+
it('NEVER AI-compresses tool results at or before the cache_control barrier', async () => {
|
|
166
|
+
// AI compression is not byte-stable → would invalidate the prompt cache.
|
|
167
|
+
// A block under (or before) the last cache_control marker must never get an
|
|
168
|
+
// [squeezr:ID] AI placeholder. Deterministic cleanup MAY touch it (it's stable).
|
|
169
|
+
const oldText = 'old line\n'.repeat(50); // compressible (dup lines) but cached
|
|
170
|
+
const newText = 'new line\n'.repeat(50);
|
|
171
|
+
const msgs = [
|
|
172
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't0', name: 'Bash' }] },
|
|
173
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't0', content: oldText, cache_control: { type: 'ephemeral' } }] },
|
|
174
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Bash' }] },
|
|
175
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: newText }] },
|
|
176
|
+
];
|
|
177
|
+
const [result, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
178
|
+
// Cached block must NOT have an AI placeholder, and cache_control must survive.
|
|
179
|
+
expect(String(result[1].content[0].content)).not.toContain('[squeezr:');
|
|
180
|
+
expect(result[1].content[0].cache_control).toEqual({ type: 'ephemeral' });
|
|
181
|
+
// AI compression count is 0 here: the only old block is the cached one (skipped),
|
|
182
|
+
// the new block is within keepRecent.
|
|
183
|
+
expect(savings.compressed).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
it('deterministic cleanup IS allowed on the cached prefix (it is byte-stable)', async () => {
|
|
186
|
+
// A block with duplicate lines under cache_control: det dedup may shrink it,
|
|
187
|
+
// but the result is identical every request → cache stays valid.
|
|
188
|
+
const dupText = 'same\n'.repeat(60); // dedup-able by deterministic pass
|
|
189
|
+
const msgs = [
|
|
190
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't0', name: 'Bash' }] },
|
|
191
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't0', content: dupText, cache_control: { type: 'ephemeral' } }] },
|
|
192
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Bash' }] },
|
|
193
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: 'tail\n'.repeat(60) }] },
|
|
194
|
+
];
|
|
195
|
+
// Run twice — the cached block's output must be byte-identical (stable).
|
|
196
|
+
const [r1] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
197
|
+
const [r2] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
198
|
+
const out1 = String(r1[1].content[0].content);
|
|
199
|
+
const out2 = String(r2[1].content[0].content);
|
|
200
|
+
expect(out1).toBe(out2); // stable between requests → cache-safe
|
|
201
|
+
expect(r1[1].content[0].cache_control).toEqual({ type: 'ephemeral' });
|
|
202
|
+
});
|
|
203
|
+
it('compresses freely when there is no cache_control marker', async () => {
|
|
204
|
+
const msgs = makeMessages(['x'.repeat(1600), 'y'.repeat(1600)]);
|
|
205
|
+
const [, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
206
|
+
// No barrier → old block is eligible for AI compression
|
|
207
|
+
expect(savings.compressed).toBe(1);
|
|
208
|
+
});
|
|
153
209
|
});
|
|
154
210
|
// ── OpenAI format ─────────────────────────────────────────────────────────────
|
|
155
211
|
describe('compressOpenAIMessages', () => {
|
|
@@ -172,16 +228,14 @@ describe('compressOpenAIMessages', () => {
|
|
|
172
228
|
expect(result).toEqual(msgs);
|
|
173
229
|
});
|
|
174
230
|
it('compresses old tool messages', async () => {
|
|
175
|
-
const
|
|
176
|
-
const msgs = makeMessages([longText, longText]);
|
|
231
|
+
const msgs = makeMessages(['y'.repeat(200), 'w'.repeat(200)]);
|
|
177
232
|
const [result, savings] = await compressOpenAIMessages(msgs, 'key', baseConfig);
|
|
178
233
|
expect(result[1].content).toContain('[squeezr:');
|
|
179
234
|
expect(savings.compressed).toBe(1);
|
|
180
235
|
});
|
|
181
236
|
it('uses Ollama backend for local keys', async () => {
|
|
182
237
|
const OpenAI = (await import('openai')).default;
|
|
183
|
-
const
|
|
184
|
-
const msgs = makeMessages([longText, longText]);
|
|
238
|
+
const msgs = makeMessages(['z'.repeat(200), 'v'.repeat(200)]);
|
|
185
239
|
await compressOpenAIMessages(msgs, 'ollama-key', { ...baseConfig, isLocalKey: () => true }, true);
|
|
186
240
|
// OpenAI client should be called (Ollama uses OpenAI-compatible API)
|
|
187
241
|
expect(OpenAI).toHaveBeenCalled();
|
|
@@ -194,11 +248,11 @@ describe('compressOpenAIMessages', () => {
|
|
|
194
248
|
expect(result).toBeDefined();
|
|
195
249
|
});
|
|
196
250
|
it('returns dry-run without modifications', async () => {
|
|
197
|
-
const
|
|
198
|
-
const msgs = makeMessages([
|
|
251
|
+
const oldText = 'z'.repeat(200);
|
|
252
|
+
const msgs = makeMessages([oldText, 'v'.repeat(200)]);
|
|
199
253
|
const [result, savings] = await compressOpenAIMessages(msgs, 'key', { ...baseConfig, dryRun: true });
|
|
200
254
|
expect(savings.dryRun).toBe(true);
|
|
201
|
-
expect(result[1].content).toBe(
|
|
255
|
+
expect(result[1].content).toBe(oldText);
|
|
202
256
|
});
|
|
203
257
|
});
|
|
204
258
|
// ── Gemini format ─────────────────────────────────────────────────────────────
|
|
@@ -220,76 +274,75 @@ describe('compressGeminiContents', () => {
|
|
|
220
274
|
expect(result).toEqual(cts);
|
|
221
275
|
});
|
|
222
276
|
it('compresses old function responses', async () => {
|
|
223
|
-
const
|
|
224
|
-
const cts = makeContents([longText, longText]);
|
|
277
|
+
const cts = makeContents(['g'.repeat(200), 'h'.repeat(200)]);
|
|
225
278
|
const [result, savings] = await compressGeminiContents(cts, 'key', baseConfig);
|
|
226
279
|
const response = result[1].parts[0].functionResponse.response;
|
|
227
280
|
expect(JSON.stringify(response)).toContain('[squeezr:');
|
|
228
281
|
expect(savings.compressed).toBe(1);
|
|
229
282
|
});
|
|
230
283
|
it('uses fetch with Gemini API URL', async () => {
|
|
231
|
-
const
|
|
232
|
-
const cts = makeContents([longText, longText]);
|
|
284
|
+
const cts = makeContents(['g'.repeat(200), 'h'.repeat(200)]);
|
|
233
285
|
await compressGeminiContents(cts, 'my-google-key', baseConfig);
|
|
234
286
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('generativelanguage.googleapis.com'), expect.any(Object));
|
|
235
287
|
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('my-google-key'), expect.any(Object));
|
|
236
288
|
});
|
|
237
289
|
it('returns dry-run without modifications', async () => {
|
|
238
|
-
const
|
|
239
|
-
const cts = makeContents([
|
|
290
|
+
const oldText = 'g'.repeat(200);
|
|
291
|
+
const cts = makeContents([oldText, 'h'.repeat(200)]);
|
|
240
292
|
const [result, savings] = await compressGeminiContents(cts, 'key', { ...baseConfig, dryRun: true });
|
|
241
293
|
expect(savings.dryRun).toBe(true);
|
|
242
294
|
const response = result[1].parts[0].functionResponse.response;
|
|
243
|
-
expect(response).toBe(
|
|
295
|
+
expect(response).toBe(oldText);
|
|
244
296
|
});
|
|
245
297
|
});
|
|
246
298
|
// ── skip_tools / only_tools / squeezr:skip ────────────────────────────────────
|
|
247
299
|
describe('skip_tools and squeezr:skip', () => {
|
|
248
|
-
|
|
300
|
+
// Per-block texts — identical blocks would be collapsed by cross-turn dedup
|
|
301
|
+
function makeMessages(toolName, textOld, textRecent) {
|
|
249
302
|
return [
|
|
250
303
|
{ role: 'assistant', content: [{ type: 'tool_use', id: 'tool_0', name: toolName }] },
|
|
251
|
-
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_0', content:
|
|
304
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_0', content: textOld }] },
|
|
252
305
|
{ role: 'assistant', content: [{ type: 'tool_use', id: 'tool_1', name: toolName }] },
|
|
253
|
-
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_1', content:
|
|
306
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_1', content: textRecent }] },
|
|
254
307
|
];
|
|
255
308
|
}
|
|
256
309
|
it('skips tool when shouldSkipTool returns true', async () => {
|
|
257
310
|
const longText = 'x'.repeat(200);
|
|
258
|
-
const msgs = makeMessages('Read', longText);
|
|
311
|
+
const msgs = makeMessages('Read', longText, 'y'.repeat(200));
|
|
259
312
|
const skipConfig = { ...baseConfig, shouldSkipTool: (t) => t.toLowerCase() === 'read' };
|
|
260
313
|
const [result, savings] = await compressAnthropicMessages(msgs, 'key', skipConfig);
|
|
261
314
|
expect(savings.compressed).toBe(0);
|
|
262
315
|
expect(result[1].content[0].content).toBe(longText);
|
|
263
316
|
});
|
|
264
317
|
it('compresses tool when shouldSkipTool returns false', async () => {
|
|
265
|
-
const
|
|
266
|
-
const msgs = makeMessages('Bash', longText);
|
|
318
|
+
const msgs = makeMessages('Bash', 'x'.repeat(1600), 'y'.repeat(1600));
|
|
267
319
|
const [, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
268
320
|
expect(savings.compressed).toBe(1);
|
|
269
321
|
});
|
|
270
322
|
it('respects squeezr:skip inline marker — does not compress that block', async () => {
|
|
271
|
-
|
|
323
|
+
// Unique text per block — identical blocks would be collapsed by cross-turn dedup
|
|
324
|
+
const skipText = 'x'.repeat(1600);
|
|
272
325
|
// 3 tool calls: tool_0 (skip marker), tool_1 (old, compressible), tool_2 (recent, kept)
|
|
273
326
|
const msgs = [
|
|
274
327
|
{
|
|
275
328
|
role: 'assistant',
|
|
276
329
|
content: [{ type: 'tool_use', id: 'tool_0', name: 'Bash', input: { command: 'git diff HEAD~3 # squeezr:skip' } }],
|
|
277
330
|
},
|
|
278
|
-
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_0', content:
|
|
331
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_0', content: skipText }] },
|
|
279
332
|
{
|
|
280
333
|
role: 'assistant',
|
|
281
334
|
content: [{ type: 'tool_use', id: 'tool_1', name: 'Bash', input: { command: 'some other command' } }],
|
|
282
335
|
},
|
|
283
|
-
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_1', content:
|
|
336
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_1', content: 'y'.repeat(1600) }] },
|
|
284
337
|
{
|
|
285
338
|
role: 'assistant',
|
|
286
339
|
content: [{ type: 'tool_use', id: 'tool_2', name: 'Bash', input: { command: 'another command' } }],
|
|
287
340
|
},
|
|
288
|
-
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_2', content:
|
|
341
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tool_2', content: 'z'.repeat(1600) }] },
|
|
289
342
|
];
|
|
290
343
|
const [result, savings] = await compressAnthropicMessages(msgs, 'key', baseConfig);
|
|
291
344
|
// tool_0 has squeezr:skip → not compressed
|
|
292
|
-
expect(result[1].content[0].content).toBe(
|
|
345
|
+
expect(result[1].content[0].content).toBe(skipText);
|
|
293
346
|
// tool_1 is old and not skipped → compressed
|
|
294
347
|
expect(savings.compressed).toBe(1);
|
|
295
348
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { compressRepeatedReads } from '../diffRead.js';
|
|
3
|
+
describe('diffRead', () => {
|
|
4
|
+
it('collapses two Reads of same path with small diff', () => {
|
|
5
|
+
const fileV1 = Array.from({ length: 100 }, (_, i) => `line ${i}`).join('\n');
|
|
6
|
+
const fileV2 = fileV1.replace('line 5', 'line 5 modified');
|
|
7
|
+
const messages = [
|
|
8
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/foo.py' } }] },
|
|
9
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: fileV1 }] },
|
|
10
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/foo.py' } }] },
|
|
11
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't2', content: fileV2 }] },
|
|
12
|
+
];
|
|
13
|
+
const r = compressRepeatedReads(messages);
|
|
14
|
+
expect(r.collapsedCount).toBe(1);
|
|
15
|
+
expect(r.savedChars).toBeGreaterThan(0);
|
|
16
|
+
const firstResult = messages[1].content[0].content;
|
|
17
|
+
expect(typeof firstResult).toBe('string');
|
|
18
|
+
expect(firstResult).toContain('squeezr_expand');
|
|
19
|
+
expect(messages[3].content[0].content).toBe(fileV2);
|
|
20
|
+
});
|
|
21
|
+
it('does NOT touch single Read', () => {
|
|
22
|
+
const content = 'X'.repeat(2000);
|
|
23
|
+
const messages = [
|
|
24
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/single.py' } }] },
|
|
25
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content }] },
|
|
26
|
+
];
|
|
27
|
+
const r = compressRepeatedReads(messages);
|
|
28
|
+
expect(r.collapsedCount).toBe(0);
|
|
29
|
+
expect(messages[1].content[0].content).toBe(content);
|
|
30
|
+
});
|
|
31
|
+
it('preserves tool_use ids and structure', () => {
|
|
32
|
+
const fileV1 = 'a\n'.repeat(300);
|
|
33
|
+
const fileV2 = fileV1 + 'extra\n';
|
|
34
|
+
const messages = [
|
|
35
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 'tA', name: 'Read', input: { file_path: '/y.py' } }] },
|
|
36
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tA', content: fileV1 }] },
|
|
37
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 'tB', name: 'Read', input: { file_path: '/y.py' } }] },
|
|
38
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tB', content: fileV2 }] },
|
|
39
|
+
];
|
|
40
|
+
compressRepeatedReads(messages);
|
|
41
|
+
expect(messages[0].content[0].id).toBe('tA');
|
|
42
|
+
expect(messages[0].content[0].name).toBe('Read');
|
|
43
|
+
expect(messages[2].content[0].id).toBe('tB');
|
|
44
|
+
expect(messages[1].content[0].tool_use_id).toBe('tA');
|
|
45
|
+
expect(messages[3].content[0].tool_use_id).toBe('tB');
|
|
46
|
+
});
|
|
47
|
+
it('skips reads with identical content', () => {
|
|
48
|
+
const same = 'identical\n'.repeat(100);
|
|
49
|
+
const messages = [
|
|
50
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/x.py' } }] },
|
|
51
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: same }] },
|
|
52
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/x.py' } }] },
|
|
53
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't2', content: same }] },
|
|
54
|
+
];
|
|
55
|
+
const r = compressRepeatedReads(messages);
|
|
56
|
+
expect(r.collapsedCount).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
it('ignores reads of different file paths', () => {
|
|
59
|
+
const text = 'L\n'.repeat(300);
|
|
60
|
+
const messages = [
|
|
61
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/a.py' } }] },
|
|
62
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: text }] },
|
|
63
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/b.py' } }] },
|
|
64
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't2', content: text }] },
|
|
65
|
+
];
|
|
66
|
+
const r = compressRepeatedReads(messages);
|
|
67
|
+
expect(r.collapsedCount).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
it('falls back to reference placeholder when diff would be too big', () => {
|
|
70
|
+
const fileV1 = Array.from({ length: 100 }, (_, i) => `line ${i} original`).join('\n');
|
|
71
|
+
const fileV2 = Array.from({ length: 100 }, (_, i) => `line ${i} totally different`).join('\n');
|
|
72
|
+
const messages = [
|
|
73
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/z.py' } }] },
|
|
74
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't1', content: fileV1 }] },
|
|
75
|
+
{ role: 'assistant', content: [{ type: 'tool_use', id: 't2', name: 'Read', input: { file_path: '/z.py' } }] },
|
|
76
|
+
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: 't2', content: fileV2 }] },
|
|
77
|
+
];
|
|
78
|
+
const r = compressRepeatedReads(messages);
|
|
79
|
+
expect(r.collapsedCount).toBe(1);
|
|
80
|
+
const placeholder = messages[1].content[0].content;
|
|
81
|
+
expect(placeholder).toContain('squeezr_expand');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|