@cnrai/pave 0.3.35 → 0.3.51
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/LICENSE +21 -0
- package/README.md +21 -218
- package/package.json +32 -35
- package/pave.js +3 -0
- package/sandbox/SandboxRunner.js +1 -0
- package/sandbox/pave-run.js +2 -0
- package/sandbox/permission.js +1 -0
- package/sandbox/utils/yaml.js +1 -0
- package/MARKETPLACE.md +0 -406
- package/build-binary.js +0 -591
- package/build-npm.js +0 -537
- package/build.js +0 -230
- package/check-binary.js +0 -26
- package/deploy.sh +0 -95
- package/index.js +0 -5776
- package/lib/agent-registry.js +0 -1037
- package/lib/args-parser.js +0 -837
- package/lib/blessed-widget-patched.js +0 -93
- package/lib/cli-markdown.js +0 -590
- package/lib/compaction.js +0 -153
- package/lib/duration.js +0 -94
- package/lib/hash.js +0 -22
- package/lib/marketplace.js +0 -866
- package/lib/memory-config.js +0 -166
- package/lib/skill-manager.js +0 -891
- package/lib/soul.js +0 -31
- package/lib/tool-output-formatter.js +0 -180
- package/start-pave.sh +0 -149
- package/status.js +0 -271
- package/test/abort-stream.test.js +0 -445
- package/test/agent-auto-compaction.test.js +0 -552
- package/test/agent-comm-abort.test.js +0 -95
- package/test/agent-comm.test.js +0 -598
- package/test/agent-inbox.test.js +0 -576
- package/test/agent-init.test.js +0 -264
- package/test/agent-interrupt.test.js +0 -314
- package/test/agent-lifecycle.test.js +0 -520
- package/test/agent-log-files.test.js +0 -349
- package/test/agent-mode.manual-test.js +0 -392
- package/test/agent-parsing.test.js +0 -228
- package/test/agent-post-stream-idle.test.js +0 -762
- package/test/agent-registry.test.js +0 -359
- package/test/agent-rm.test.js +0 -442
- package/test/agent-spawn.test.js +0 -933
- package/test/agent-status-api.test.js +0 -624
- package/test/agent-update.test.js +0 -435
- package/test/args-parser.test.js +0 -391
- package/test/auto-compaction-chat.manual-test.js +0 -227
- package/test/auto-compaction.test.js +0 -941
- package/test/build-config.test.js +0 -120
- package/test/build-npm.test.js +0 -388
- package/test/chat-command.test.js +0 -137
- package/test/chat-leading-lines.test.js +0 -159
- package/test/config-flag.test.js +0 -272
- package/test/cursor-drift.test.js +0 -135
- package/test/debug-require.js +0 -23
- package/test/dir-migration.test.js +0 -323
- package/test/duration.test.js +0 -229
- package/test/ghostty-term.test.js +0 -202
- package/test/http500-backoff.test.js +0 -854
- package/test/integration.test.js +0 -86
- package/test/memory-guard-env.test.js +0 -220
- package/test/pr233-fixes.test.js +0 -259
- package/test/run-agent-init.js +0 -297
- package/test/run-all.js +0 -64
- package/test/run-config-flag.js +0 -159
- package/test/run-cursor-drift.js +0 -82
- package/test/run-session-path.js +0 -154
- package/test/run-tests.js +0 -643
- package/test/sandbox-redirect.test.js +0 -202
- package/test/session-path.test.js +0 -132
- package/test/shebang-strip.test.js +0 -241
- package/test/soul-reinject.test.js +0 -1027
- package/test/soul-reread.test.js +0 -281
- package/test/tool-output-formatter.test.js +0 -486
- package/test/tool-output-gating.test.js +0 -143
- package/test/tool-states.test.js +0 -167
- package/test/tools-flag.test.js +0 -65
- package/test/tui-attach.test.js +0 -1255
- package/test/tui-compaction.test.js +0 -354
- package/test/tui-wrap.test.js +0 -568
- package/test-binary.js +0 -52
- package/test-binary2.js +0 -36
|
@@ -1,941 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for auto-compaction feature
|
|
4
|
-
* Tests model parsing and compaction response handling logic
|
|
5
|
-
* Run with: node test/auto-compaction.test.js
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// Import shared helper with sandbox fallback
|
|
10
|
-
// ============================================================================
|
|
11
|
-
// Try to import from lib/compaction.js first (normal Node execution).
|
|
12
|
-
// Fall back to inline implementation for sandbox environments where the
|
|
13
|
-
// relative require path doesn't work (test files are copied to temp directory).
|
|
14
|
-
let parseModelForCompaction;
|
|
15
|
-
try {
|
|
16
|
-
const compaction = require('../lib/compaction');
|
|
17
|
-
parseModelForCompaction = compaction.parseModelForCompaction;
|
|
18
|
-
} catch (e) {
|
|
19
|
-
// Fallback for sandbox: simplified inline version of lib/compaction.js.
|
|
20
|
-
// This uses hardcoded defaults (no env var support) since sandbox tests
|
|
21
|
-
// cannot modify process.env. See lib/compaction.js for the canonical
|
|
22
|
-
// implementation with full PAVE_COMPACT_MODEL/PAVE_MODEL precedence.
|
|
23
|
-
const KNOWN_COMPACTION_MODELS = [
|
|
24
|
-
'claude-opus-4.5',
|
|
25
|
-
'claude-opus-4.6',
|
|
26
|
-
'claude-sonnet-4', // Canonical model advertised in CLI help
|
|
27
|
-
'claude-sonnet-4.5',
|
|
28
|
-
'claude-sonnet-4.6',
|
|
29
|
-
'claude-3-5-sonnet-20241022',
|
|
30
|
-
'claude-3-5-haiku-20241022',
|
|
31
|
-
'claude-3-opus-20240229',
|
|
32
|
-
'gpt-4',
|
|
33
|
-
'gpt-4-turbo',
|
|
34
|
-
'gpt-3.5-turbo',
|
|
35
|
-
];
|
|
36
|
-
const DEFAULT_PROVIDER_ID = 'github-copilot';
|
|
37
|
-
const DEFAULT_MODEL_ID = 'claude-opus-4.5';
|
|
38
|
-
|
|
39
|
-
parseModelForCompaction = function (model) {
|
|
40
|
-
let providerID = DEFAULT_PROVIDER_ID;
|
|
41
|
-
let modelID = DEFAULT_MODEL_ID;
|
|
42
|
-
|
|
43
|
-
if (model) {
|
|
44
|
-
if (model.includes('/')) {
|
|
45
|
-
const parts = model.split('/');
|
|
46
|
-
providerID = parts[0];
|
|
47
|
-
modelID = parts.slice(1).join('/');
|
|
48
|
-
} else {
|
|
49
|
-
modelID = model;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const compactionModelID = KNOWN_COMPACTION_MODELS.includes(modelID) ? modelID : DEFAULT_MODEL_ID;
|
|
54
|
-
return { providerID, modelID, compactionModelID };
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Simple URL parser for testing (avoids dependency on 'url' module in sandboxed environments)
|
|
59
|
-
function parseURL(urlString) {
|
|
60
|
-
// Extract pathname and search from URL string
|
|
61
|
-
const match = urlString.match(/^https?:\/\/[^\/]+(\/[^?]*)?(\?.*)?$/);
|
|
62
|
-
if (!match) return { pathname: '/', search: '' };
|
|
63
|
-
return {
|
|
64
|
-
pathname: match[1] || '/',
|
|
65
|
-
search: match[2] || '',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function runTest(name, testFn) {
|
|
70
|
-
try {
|
|
71
|
-
testFn();
|
|
72
|
-
console.log(`✅ ${name}`);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
console.log(`❌ ${name}: ${error.message}`);
|
|
75
|
-
process.exitCode = 1;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function assertEqual(actual, expected, message) {
|
|
80
|
-
if (actual !== expected) {
|
|
81
|
-
throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function assertDeepEqual(actual, expected, message) {
|
|
86
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
87
|
-
throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Alias for parseModelForCompaction - matches the test naming convention
|
|
93
|
-
*/
|
|
94
|
-
const parseModelString = parseModelForCompaction;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Extract compaction fields from API response
|
|
98
|
-
* This mirrors the field access pattern used in handleChatCommand
|
|
99
|
-
*/
|
|
100
|
-
function extractCompactionFields(compactionCheck) {
|
|
101
|
-
if (!compactionCheck) {
|
|
102
|
-
return { needed: false, totalTokens: undefined, threshold: undefined };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
needed: compactionCheck.needed || false,
|
|
107
|
-
totalTokens: compactionCheck.totalTokens,
|
|
108
|
-
threshold: compactionCheck.usage?.limits?.threshold,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Calculate token savings safely
|
|
114
|
-
* Returns null if values are invalid or savings is not positive
|
|
115
|
-
*/
|
|
116
|
-
function calculateTokenSavings(originalTokens, summaryTokens) {
|
|
117
|
-
const summary = summaryTokens || 0;
|
|
118
|
-
const saved = originalTokens - summary;
|
|
119
|
-
|
|
120
|
-
if (!originalTokens || saved <= 0) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { originalTokens, summaryTokens: summary, savedTokens: saved };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ============== Model Parsing Tests ==============
|
|
128
|
-
|
|
129
|
-
runTest('parseModelString: should return defaults when model is undefined', () => {
|
|
130
|
-
const result = parseModelString(undefined);
|
|
131
|
-
assertEqual(result.providerID, 'github-copilot', 'providerID');
|
|
132
|
-
assertEqual(result.modelID, 'claude-opus-4.5', 'modelID');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
runTest('parseModelString: should return defaults when model is empty string', () => {
|
|
136
|
-
const result = parseModelString('');
|
|
137
|
-
assertEqual(result.providerID, 'github-copilot', 'providerID');
|
|
138
|
-
assertEqual(result.modelID, 'claude-opus-4.5', 'modelID');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
runTest('parseModelString: should parse provider/model format correctly', () => {
|
|
142
|
-
const result = parseModelString('github-copilot/claude-sonnet-4');
|
|
143
|
-
assertEqual(result.providerID, 'github-copilot', 'providerID');
|
|
144
|
-
assertEqual(result.modelID, 'claude-sonnet-4', 'modelID');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
runTest('parseModelString: should handle model without provider (uses default provider)', () => {
|
|
148
|
-
const result = parseModelString('gpt-4');
|
|
149
|
-
assertEqual(result.providerID, 'github-copilot', 'providerID');
|
|
150
|
-
assertEqual(result.modelID, 'gpt-4', 'modelID');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
runTest('parseModelString: should handle models with multiple slashes', () => {
|
|
154
|
-
const result = parseModelString('openai/gpt-4/turbo');
|
|
155
|
-
assertEqual(result.providerID, 'openai', 'providerID');
|
|
156
|
-
assertEqual(result.modelID, 'gpt-4/turbo', 'modelID');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
runTest('parseModelString: should handle different providers', () => {
|
|
160
|
-
const result = parseModelString('anthropic/claude-3-opus');
|
|
161
|
-
assertEqual(result.providerID, 'anthropic', 'providerID');
|
|
162
|
-
assertEqual(result.modelID, 'claude-3-opus', 'modelID');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ============== Compaction Response Field Tests ==============
|
|
166
|
-
|
|
167
|
-
runTest('extractCompactionFields: should handle null response', () => {
|
|
168
|
-
const result = extractCompactionFields(null);
|
|
169
|
-
assertEqual(result.needed, false, 'needed');
|
|
170
|
-
assertEqual(result.totalTokens, undefined, 'totalTokens');
|
|
171
|
-
assertEqual(result.threshold, undefined, 'threshold');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
runTest('extractCompactionFields: should extract fields from valid response', () => {
|
|
175
|
-
const response = {
|
|
176
|
-
needed: true,
|
|
177
|
-
totalTokens: 150000,
|
|
178
|
-
usage: {
|
|
179
|
-
limits: {
|
|
180
|
-
threshold: 100000,
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
const result = extractCompactionFields(response);
|
|
185
|
-
assertEqual(result.needed, true, 'needed');
|
|
186
|
-
assertEqual(result.totalTokens, 150000, 'totalTokens');
|
|
187
|
-
assertEqual(result.threshold, 100000, 'threshold');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
runTest('extractCompactionFields: should handle response without usage.limits', () => {
|
|
191
|
-
const response = {
|
|
192
|
-
needed: false,
|
|
193
|
-
totalTokens: 50000,
|
|
194
|
-
};
|
|
195
|
-
const result = extractCompactionFields(response);
|
|
196
|
-
assertEqual(result.needed, false, 'needed');
|
|
197
|
-
assertEqual(result.totalTokens, 50000, 'totalTokens');
|
|
198
|
-
assertEqual(result.threshold, undefined, 'threshold');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
runTest('extractCompactionFields: should use correct field name "needed" not "needsCompaction"', () => {
|
|
202
|
-
// This test ensures we use the correct API field name
|
|
203
|
-
const responseWithWrongField = {
|
|
204
|
-
needsCompaction: true, // Wrong field name
|
|
205
|
-
totalTokens: 150000,
|
|
206
|
-
};
|
|
207
|
-
const result = extractCompactionFields(responseWithWrongField);
|
|
208
|
-
assertEqual(result.needed, false, 'needed should be false when using wrong field name');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// ============== Token Savings Calculation Tests ==============
|
|
212
|
-
|
|
213
|
-
runTest('calculateTokenSavings: should return null when originalTokens is undefined', () => {
|
|
214
|
-
const result = calculateTokenSavings(undefined, 1000);
|
|
215
|
-
assertEqual(result, null, 'result');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
runTest('calculateTokenSavings: should return null when originalTokens is 0', () => {
|
|
219
|
-
const result = calculateTokenSavings(0, 1000);
|
|
220
|
-
assertEqual(result, null, 'result');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
runTest('calculateTokenSavings: should return null when savings is negative', () => {
|
|
224
|
-
const result = calculateTokenSavings(1000, 2000);
|
|
225
|
-
assertEqual(result, null, 'result');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
runTest('calculateTokenSavings: should return null when savings is zero', () => {
|
|
229
|
-
const result = calculateTokenSavings(1000, 1000);
|
|
230
|
-
assertEqual(result, null, 'result');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
runTest('calculateTokenSavings: should calculate positive savings correctly', () => {
|
|
234
|
-
const result = calculateTokenSavings(150000, 5000);
|
|
235
|
-
assertDeepEqual(result, {
|
|
236
|
-
originalTokens: 150000,
|
|
237
|
-
summaryTokens: 5000,
|
|
238
|
-
savedTokens: 145000,
|
|
239
|
-
}, 'result');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
runTest('calculateTokenSavings: should handle undefined summaryTokens as 0', () => {
|
|
243
|
-
const result = calculateTokenSavings(100000, undefined);
|
|
244
|
-
assertDeepEqual(result, {
|
|
245
|
-
originalTokens: 100000,
|
|
246
|
-
summaryTokens: 0,
|
|
247
|
-
savedTokens: 100000,
|
|
248
|
-
}, 'result');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
runTest('calculateTokenSavings: should handle null summaryTokens as 0', () => {
|
|
252
|
-
const result = calculateTokenSavings(100000, null);
|
|
253
|
-
assertDeepEqual(result, {
|
|
254
|
-
originalTokens: 100000,
|
|
255
|
-
summaryTokens: 0,
|
|
256
|
-
savedTokens: 100000,
|
|
257
|
-
}, 'result');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ============== URL Query String Tests ==============
|
|
261
|
-
|
|
262
|
-
runTest('URL should include query string in path', () => {
|
|
263
|
-
// This test verifies the fix for httpRequest not including urlObj.search
|
|
264
|
-
const url = 'http://localhost:4096/api/sessions/123/compaction/check?modelID=claude-opus-4.5';
|
|
265
|
-
const urlObj = parseURL(url);
|
|
266
|
-
const path = urlObj.pathname + urlObj.search;
|
|
267
|
-
|
|
268
|
-
assertEqual(path, '/api/sessions/123/compaction/check?modelID=claude-opus-4.5', 'path should include query string');
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
runTest('URL path should work with encoded query parameters', () => {
|
|
272
|
-
const modelID = 'claude-sonnet-4';
|
|
273
|
-
const url = `http://localhost:4096/api/sessions/123/compaction/check?modelID=${encodeURIComponent(modelID)}`;
|
|
274
|
-
const urlObj = parseURL(url);
|
|
275
|
-
const path = urlObj.pathname + urlObj.search;
|
|
276
|
-
|
|
277
|
-
assertEqual(path, '/api/sessions/123/compaction/check?modelID=claude-sonnet-4', 'path should include encoded query');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
runTest('URL path should handle special characters in modelID', () => {
|
|
281
|
-
const modelID = 'gpt-4/turbo';
|
|
282
|
-
const url = `http://localhost:4096/api/sessions/123/compaction/check?modelID=${encodeURIComponent(modelID)}`;
|
|
283
|
-
const urlObj = parseURL(url);
|
|
284
|
-
const path = urlObj.pathname + urlObj.search;
|
|
285
|
-
|
|
286
|
-
assertEqual(path, '/api/sessions/123/compaction/check?modelID=gpt-4%2Fturbo', 'path should properly encode slashes');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ============== Integration Tests with Stubbed httpRequest ==============
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Simulates the auto-compaction flow with stubbed httpRequest
|
|
293
|
-
* This tests the integration between compaction check and compact operations
|
|
294
|
-
*/
|
|
295
|
-
async function runAsyncTest(name, testFn) {
|
|
296
|
-
try {
|
|
297
|
-
await testFn();
|
|
298
|
-
console.log(`✅ ${name}`);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
console.log(`❌ ${name}: ${error.message}`);
|
|
301
|
-
process.exitCode = 1;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Simulates the auto-compaction flow logic from handleChatCommand
|
|
307
|
-
* Uses stubbed httpRequest to verify correct API calls are made
|
|
308
|
-
*/
|
|
309
|
-
async function simulateAutoCompaction(options) {
|
|
310
|
-
const {
|
|
311
|
-
httpRequestStub,
|
|
312
|
-
serverUrl = 'http://localhost:4096',
|
|
313
|
-
sessionId = 'test-session-123',
|
|
314
|
-
model = 'github-copilot/claude-sonnet-4',
|
|
315
|
-
verbose = false,
|
|
316
|
-
isNewSession = false, // Track if session was just created (skips compaction check)
|
|
317
|
-
} = options;
|
|
318
|
-
|
|
319
|
-
const logs = [];
|
|
320
|
-
const mockConsoleError = (msg) => logs.push(msg);
|
|
321
|
-
|
|
322
|
-
// Use shared helper for model parsing (same as production code)
|
|
323
|
-
const { providerID, modelID, compactionModelID } = parseModelString(model);
|
|
324
|
-
|
|
325
|
-
let currentSessionId = sessionId;
|
|
326
|
-
let compactionCheck = null;
|
|
327
|
-
let compactionResult = null;
|
|
328
|
-
const apiCalls = [];
|
|
329
|
-
let compactionSkipped = false; // Track if compaction was skipped due to new session
|
|
330
|
-
|
|
331
|
-
// Step 1: Check if compaction is needed (uses compactionModelID, not raw modelID)
|
|
332
|
-
// Skip compaction check on new session - no messages exist yet,
|
|
333
|
-
// so the server would always return needed: false (wasted HTTP round-trip)
|
|
334
|
-
if (!isNewSession) {
|
|
335
|
-
try {
|
|
336
|
-
const checkUrl = `${serverUrl}/api/sessions/${currentSessionId}/compaction/check?modelID=${encodeURIComponent(compactionModelID)}`;
|
|
337
|
-
apiCalls.push({ method: 'GET', url: checkUrl, originalModelID: modelID, compactionModelID });
|
|
338
|
-
compactionCheck = await httpRequestStub(checkUrl, 'GET');
|
|
339
|
-
} catch (checkError) {
|
|
340
|
-
if (verbose) {
|
|
341
|
-
mockConsoleError(`Compaction check failed: ${checkError.message}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
compactionSkipped = true;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Step 2: Perform compaction if needed
|
|
349
|
-
if (compactionCheck && compactionCheck.needed) {
|
|
350
|
-
const threshold = compactionCheck.usage?.limits?.threshold;
|
|
351
|
-
if (verbose) {
|
|
352
|
-
mockConsoleError(`Auto-compaction triggered: ${compactionCheck.totalTokens} > ${threshold} tokens`);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
const compactUrl = `${serverUrl}/api/sessions/${currentSessionId}/compaction/compact`;
|
|
357
|
-
const compactBody = {
|
|
358
|
-
title: `Auto-compacted Session (${new Date().toISOString().split('T')[0]})`,
|
|
359
|
-
auto: true,
|
|
360
|
-
force: false,
|
|
361
|
-
model: { providerID, modelID },
|
|
362
|
-
};
|
|
363
|
-
apiCalls.push({ method: 'POST', url: compactUrl, body: compactBody });
|
|
364
|
-
compactionResult = await httpRequestStub(compactUrl, 'POST', compactBody);
|
|
365
|
-
|
|
366
|
-
if (compactionResult && compactionResult.success) {
|
|
367
|
-
// Validate newSessionID before switching - keep original if invalid
|
|
368
|
-
const newSessionID = compactionResult.newSessionID;
|
|
369
|
-
if (!newSessionID || typeof newSessionID !== 'string' || newSessionID.trim() === '') {
|
|
370
|
-
// Always show this warning (not gated by verbose) - matches production
|
|
371
|
-
mockConsoleError(`Auto-compaction warning: server returned success but invalid newSessionID, keeping original session`);
|
|
372
|
-
// Continue with original session
|
|
373
|
-
} else {
|
|
374
|
-
// Switch to the new compacted session
|
|
375
|
-
const originalTokens = compactionCheck.totalTokens;
|
|
376
|
-
currentSessionId = newSessionID;
|
|
377
|
-
|
|
378
|
-
// Calculate token savings - check for actual summaryTokens availability
|
|
379
|
-
// Don't use || 0 fallback so we can detect when summaryTokens is not provided
|
|
380
|
-
const summaryTokens = compactionResult.summaryTokens;
|
|
381
|
-
const hasSummaryTokens = typeof summaryTokens === 'number';
|
|
382
|
-
const savedTokens = hasSummaryTokens ? originalTokens - summaryTokens : 0;
|
|
383
|
-
|
|
384
|
-
if (verbose) {
|
|
385
|
-
mockConsoleError(`Auto-compaction completed: switched to session ${currentSessionId}`);
|
|
386
|
-
if (originalTokens && savedTokens > 0) {
|
|
387
|
-
mockConsoleError(`Saved ${savedTokens} tokens`);
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
if (originalTokens && hasSummaryTokens) {
|
|
391
|
-
mockConsoleError(`Auto-compacted conversation (${originalTokens} → ${summaryTokens} tokens)`);
|
|
392
|
-
} else {
|
|
393
|
-
mockConsoleError(`Auto-compacted conversation to new session`);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
// Always show compaction failure warnings (not gated by verbose)
|
|
399
|
-
mockConsoleError(`Auto-compaction failed: ${compactionResult?.error || 'unknown error'}`);
|
|
400
|
-
}
|
|
401
|
-
} catch (compactError) {
|
|
402
|
-
// Always show compaction failure warnings (not gated by verbose)
|
|
403
|
-
mockConsoleError(`Compaction execution failed: ${compactError.message}`);
|
|
404
|
-
}
|
|
405
|
-
} else if (verbose && compactionCheck) {
|
|
406
|
-
const totalTokens = compactionCheck.totalTokens;
|
|
407
|
-
const threshold = compactionCheck.usage?.limits?.threshold;
|
|
408
|
-
if (typeof totalTokens === 'number' && typeof threshold === 'number') {
|
|
409
|
-
mockConsoleError(`No compaction needed: ${totalTokens}/${threshold} tokens`);
|
|
410
|
-
} else if (compactionCheck.reason || compactionCheck.error) {
|
|
411
|
-
mockConsoleError(`Compaction check: ${compactionCheck.reason || compactionCheck.error}`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
sessionId: currentSessionId,
|
|
417
|
-
apiCalls,
|
|
418
|
-
logs,
|
|
419
|
-
compactionCheck,
|
|
420
|
-
compactionResult,
|
|
421
|
-
compactionSkipped, // True if compaction was skipped due to new session
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Wrap all async tests in a main function to ensure proper awaiting
|
|
426
|
-
async function runIntegrationTests() {
|
|
427
|
-
// Integration test: Full compaction flow
|
|
428
|
-
await runAsyncTest('Integration: should call correct APIs when compaction is needed', async () => {
|
|
429
|
-
const httpRequestStub = async (url, method, body) => {
|
|
430
|
-
if (url.includes('/compaction/check')) {
|
|
431
|
-
return {
|
|
432
|
-
needed: true,
|
|
433
|
-
totalTokens: 150000,
|
|
434
|
-
usage: { limits: { threshold: 100000 } },
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
if (url.includes('/compaction/compact')) {
|
|
438
|
-
return {
|
|
439
|
-
success: true,
|
|
440
|
-
newSessionID: 'new-compacted-session-456',
|
|
441
|
-
summaryTokens: 5000,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
const result = await simulateAutoCompaction({
|
|
448
|
-
httpRequestStub,
|
|
449
|
-
model: 'github-copilot/gpt-4', // Use a known model from the registry
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
assertEqual(result.apiCalls.length, 2, 'should make 2 API calls');
|
|
453
|
-
assertEqual(result.apiCalls[0].method, 'GET', 'first call should be GET');
|
|
454
|
-
if (!result.apiCalls[0].url.includes('/compaction/check')) {
|
|
455
|
-
throw new Error('first call should be to compaction/check');
|
|
456
|
-
}
|
|
457
|
-
if (!result.apiCalls[0].url.includes('modelID=gpt-4')) {
|
|
458
|
-
throw new Error('check URL should include parsed modelID');
|
|
459
|
-
}
|
|
460
|
-
assertEqual(result.apiCalls[1].method, 'POST', 'second call should be POST');
|
|
461
|
-
if (!result.apiCalls[1].url.includes('/compaction/compact')) {
|
|
462
|
-
throw new Error('second call should be to compaction/compact');
|
|
463
|
-
}
|
|
464
|
-
assertEqual(result.apiCalls[1].body.model.providerID, 'github-copilot', 'compact body should have providerID');
|
|
465
|
-
assertEqual(result.apiCalls[1].body.model.modelID, 'gpt-4', 'compact body should have modelID');
|
|
466
|
-
assertEqual(result.sessionId, 'new-compacted-session-456', 'session should be updated');
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Integration test: New session optimization - skip compaction check
|
|
470
|
-
await runAsyncTest('Integration: should skip compaction check for new session', async () => {
|
|
471
|
-
const httpRequestStub = async (url, method, body) => {
|
|
472
|
-
// This should NOT be called for new session
|
|
473
|
-
throw new Error(`Unexpected API call: ${url} - should have been skipped for new session`);
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
const result = await simulateAutoCompaction({
|
|
477
|
-
httpRequestStub,
|
|
478
|
-
isNewSession: true, // New session flag
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
assertEqual(result.apiCalls.length, 0, 'should make no API calls for new session');
|
|
482
|
-
assertEqual(result.compactionSkipped, true, 'compactionSkipped should be true');
|
|
483
|
-
assertEqual(result.compactionCheck, null, 'compactionCheck should be null');
|
|
484
|
-
assertEqual(result.sessionId, 'test-session-123', 'session should not change');
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// Integration test: Existing session should still check compaction
|
|
488
|
-
await runAsyncTest('Integration: should check compaction for existing session', async () => {
|
|
489
|
-
const httpRequestStub = async (url, method, body) => {
|
|
490
|
-
if (url.includes('/compaction/check')) {
|
|
491
|
-
return { needed: false, totalTokens: 50000, usage: { limits: { threshold: 100000 } } };
|
|
492
|
-
}
|
|
493
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
const result = await simulateAutoCompaction({
|
|
497
|
-
httpRequestStub,
|
|
498
|
-
isNewSession: false, // Existing session (default)
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
assertEqual(result.apiCalls.length, 1, 'should make 1 API call for existing session');
|
|
502
|
-
assertEqual(result.compactionSkipped, false, 'compactionSkipped should be false');
|
|
503
|
-
assertEqual(result.compactionCheck.needed, false, 'compactionCheck should have response');
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// Integration test: No compaction needed
|
|
507
|
-
await runAsyncTest('Integration: should only call check API when compaction not needed', async () => {
|
|
508
|
-
const httpRequestStub = async (url, method, body) => {
|
|
509
|
-
if (url.includes('/compaction/check')) {
|
|
510
|
-
return {
|
|
511
|
-
needed: false,
|
|
512
|
-
totalTokens: 50000,
|
|
513
|
-
usage: { limits: { threshold: 100000 } },
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
const result = await simulateAutoCompaction({
|
|
520
|
-
httpRequestStub,
|
|
521
|
-
verbose: true,
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
assertEqual(result.apiCalls.length, 1, 'should make only 1 API call');
|
|
525
|
-
assertEqual(result.sessionId, 'test-session-123', 'session should not change');
|
|
526
|
-
if (!result.logs.some((l) => l.includes('No compaction needed'))) {
|
|
527
|
-
throw new Error('should log "No compaction needed"');
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Integration test: Check fails gracefully
|
|
532
|
-
await runAsyncTest('Integration: should continue when check fails', async () => {
|
|
533
|
-
const httpRequestStub = async (url, method, body) => {
|
|
534
|
-
if (url.includes('/compaction/check')) {
|
|
535
|
-
throw new Error('Network error');
|
|
536
|
-
}
|
|
537
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
const result = await simulateAutoCompaction({
|
|
541
|
-
httpRequestStub,
|
|
542
|
-
verbose: true,
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
assertEqual(result.apiCalls.length, 1, 'should attempt 1 API call');
|
|
546
|
-
assertEqual(result.sessionId, 'test-session-123', 'session should not change');
|
|
547
|
-
if (!result.logs.some((l) => l.includes('Compaction check failed'))) {
|
|
548
|
-
throw new Error('should log "Compaction check failed"');
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
// Integration test: Compact fails gracefully
|
|
553
|
-
await runAsyncTest('Integration: should continue when compact fails', async () => {
|
|
554
|
-
const httpRequestStub = async (url, method, body) => {
|
|
555
|
-
if (url.includes('/compaction/check')) {
|
|
556
|
-
return {
|
|
557
|
-
needed: true,
|
|
558
|
-
totalTokens: 150000,
|
|
559
|
-
usage: { limits: { threshold: 100000 } },
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
if (url.includes('/compaction/compact')) {
|
|
563
|
-
throw new Error('Server error 500');
|
|
564
|
-
}
|
|
565
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
566
|
-
};
|
|
567
|
-
|
|
568
|
-
const result = await simulateAutoCompaction({
|
|
569
|
-
httpRequestStub,
|
|
570
|
-
verbose: true,
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
assertEqual(result.apiCalls.length, 2, 'should attempt 2 API calls');
|
|
574
|
-
assertEqual(result.sessionId, 'test-session-123', 'session should not change on failure');
|
|
575
|
-
if (!result.logs.some((l) => l.includes('Compaction execution failed'))) {
|
|
576
|
-
throw new Error('should log "Compaction execution failed" (not "check failed")');
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
// Integration test: Verbose logging with undefined values
|
|
581
|
-
await runAsyncTest('Integration: should not log undefined/undefined when check returns error', async () => {
|
|
582
|
-
const httpRequestStub = async (url, method, body) => {
|
|
583
|
-
if (url.includes('/compaction/check')) {
|
|
584
|
-
return {
|
|
585
|
-
needed: false,
|
|
586
|
-
reason: 'Session not found',
|
|
587
|
-
error: 'SESSION_NOT_FOUND',
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
const result = await simulateAutoCompaction({
|
|
594
|
-
httpRequestStub,
|
|
595
|
-
verbose: true,
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
// Should not log "undefined/undefined tokens"
|
|
599
|
-
if (result.logs.some((l) => l.includes('undefined'))) {
|
|
600
|
-
throw new Error('should not log undefined values');
|
|
601
|
-
}
|
|
602
|
-
// Should log the reason/error instead
|
|
603
|
-
if (!result.logs.some((l) => l.includes('SESSION_NOT_FOUND') || l.includes('Session not found'))) {
|
|
604
|
-
throw new Error('should log error reason when available');
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
// Integration test: Model parsing in API calls
|
|
609
|
-
await runAsyncTest('Integration: should parse model-only format correctly', async () => {
|
|
610
|
-
const httpRequestStub = async (url, method, body) => {
|
|
611
|
-
if (url.includes('/compaction/check')) {
|
|
612
|
-
return { needed: false, totalTokens: 1000, usage: { limits: { threshold: 100000 } } };
|
|
613
|
-
}
|
|
614
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
const result = await simulateAutoCompaction({
|
|
618
|
-
httpRequestStub,
|
|
619
|
-
model: 'gpt-4', // No provider prefix
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// Should use default provider and the provided model
|
|
623
|
-
if (!result.apiCalls[0].url.includes('modelID=gpt-4')) {
|
|
624
|
-
throw new Error('should use model name from input');
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// Integration test: Unknown model should fallback to known model for compaction check
|
|
629
|
-
await runAsyncTest('Integration: should fallback to known model for unknown modelID', async () => {
|
|
630
|
-
const httpRequestStub = async (url, method, body) => {
|
|
631
|
-
if (url.includes('/compaction/check')) {
|
|
632
|
-
return { needed: false, totalTokens: 1000, usage: { limits: { threshold: 100000 } } };
|
|
633
|
-
}
|
|
634
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
// Simulate with unknown model - should fallback to known model
|
|
638
|
-
const result = await simulateAutoCompaction({
|
|
639
|
-
httpRequestStub,
|
|
640
|
-
model: 'github-copilot/some-unknown-model',
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
assertEqual(result.apiCalls.length, 1, 'should make 1 API call');
|
|
644
|
-
|
|
645
|
-
// Verify the fallback: URL should use claude-opus-4.5 (fallback), not some-unknown-model
|
|
646
|
-
const checkUrl = result.apiCalls[0].url;
|
|
647
|
-
if (checkUrl.includes('modelID=some-unknown-model')) {
|
|
648
|
-
throw new Error('should NOT use unknown model in URL');
|
|
649
|
-
}
|
|
650
|
-
if (!checkUrl.includes('modelID=claude-opus-4.5')) {
|
|
651
|
-
throw new Error('should use fallback model claude-opus-4.5 in URL');
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Also verify via apiCalls metadata
|
|
655
|
-
assertEqual(result.apiCalls[0].originalModelID, 'some-unknown-model', 'should track original model');
|
|
656
|
-
assertEqual(result.apiCalls[0].compactionModelID, 'claude-opus-4.5', 'should use fallback for compaction');
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// Integration test: Known model should NOT fallback
|
|
660
|
-
await runAsyncTest('Integration: should use known model directly without fallback', async () => {
|
|
661
|
-
const httpRequestStub = async (url, method, body) => {
|
|
662
|
-
if (url.includes('/compaction/check')) {
|
|
663
|
-
return { needed: false, totalTokens: 1000, usage: { limits: { threshold: 100000 } } };
|
|
664
|
-
}
|
|
665
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
// Use a known model - should use it directly
|
|
669
|
-
const result = await simulateAutoCompaction({
|
|
670
|
-
httpRequestStub,
|
|
671
|
-
model: 'github-copilot/gpt-4',
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Verify the URL uses gpt-4 directly (no fallback)
|
|
675
|
-
const checkUrl = result.apiCalls[0].url;
|
|
676
|
-
if (!checkUrl.includes('modelID=gpt-4')) {
|
|
677
|
-
throw new Error('should use known model gpt-4 directly in URL');
|
|
678
|
-
}
|
|
679
|
-
assertEqual(result.apiCalls[0].originalModelID, 'gpt-4', 'original model');
|
|
680
|
-
assertEqual(result.apiCalls[0].compactionModelID, 'gpt-4', 'should NOT fallback for known model');
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// Integration test: Should not switch session if newSessionID is invalid
|
|
684
|
-
await runAsyncTest('Integration: should keep original session if newSessionID is invalid', async () => {
|
|
685
|
-
const httpRequestStub = async (url, method, body) => {
|
|
686
|
-
if (url.includes('/compaction/check')) {
|
|
687
|
-
return {
|
|
688
|
-
needed: true,
|
|
689
|
-
totalTokens: 150000,
|
|
690
|
-
usage: { limits: { threshold: 100000 } },
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
if (url.includes('/compaction/compact')) {
|
|
694
|
-
// Server returns success but with empty/invalid newSessionID
|
|
695
|
-
return {
|
|
696
|
-
success: true,
|
|
697
|
-
newSessionID: '', // Invalid!
|
|
698
|
-
summaryTokens: 5000,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
const result = await simulateAutoCompaction({
|
|
705
|
-
httpRequestStub,
|
|
706
|
-
verbose: true,
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
// Session should NOT change when newSessionID is invalid
|
|
710
|
-
assertEqual(result.sessionId, 'test-session-123', 'session should not change with invalid newSessionID');
|
|
711
|
-
// Should log a warning
|
|
712
|
-
if (!result.logs.some((l) => l.includes('invalid newSessionID') || l.includes('keeping original'))) {
|
|
713
|
-
throw new Error('should log warning about invalid newSessionID');
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// ============================================================================
|
|
718
|
-
// Non-verbose failure path tests
|
|
719
|
-
// These tests verify that compaction failure messages are always shown,
|
|
720
|
-
// even when verbose is false. This ensures future changes don't accidentally
|
|
721
|
-
// re-introduce verbose-gating on the failure paths.
|
|
722
|
-
// ============================================================================
|
|
723
|
-
|
|
724
|
-
// Test: Non-verbose mode - compaction execution failure should always be shown
|
|
725
|
-
await runAsyncTest('Integration: should always show execution failure even when verbose is false', async () => {
|
|
726
|
-
const httpRequestStub = async (url, method, body) => {
|
|
727
|
-
if (url.includes('/compaction/check')) {
|
|
728
|
-
return { needed: true, totalTokens: 150000, usage: { limits: { threshold: 100000 } } };
|
|
729
|
-
}
|
|
730
|
-
if (url.includes('/compaction/compact')) {
|
|
731
|
-
throw new Error('Network timeout');
|
|
732
|
-
}
|
|
733
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
const result = await simulateAutoCompaction({
|
|
737
|
-
httpRequestStub,
|
|
738
|
-
verbose: false, // Non-verbose mode
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// Should always show execution failure message even without verbose
|
|
742
|
-
if (!result.logs.some((l) => l.includes('Compaction execution failed'))) {
|
|
743
|
-
throw new Error(`should show execution failure message even in non-verbose mode, got: ${result.logs.join(', ')}`);
|
|
744
|
-
}
|
|
745
|
-
if (!result.logs.some((l) => l.includes('Network timeout'))) {
|
|
746
|
-
throw new Error(`should include error message, got: ${result.logs.join(', ')}`);
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
// Test: Non-verbose mode - compaction API failure should always be shown
|
|
751
|
-
await runAsyncTest('Integration: should always show API failure even when verbose is false', async () => {
|
|
752
|
-
const httpRequestStub = async (url, method, body) => {
|
|
753
|
-
if (url.includes('/compaction/check')) {
|
|
754
|
-
return { needed: true, totalTokens: 150000, usage: { limits: { threshold: 100000 } } };
|
|
755
|
-
}
|
|
756
|
-
if (url.includes('/compaction/compact')) {
|
|
757
|
-
// Server returns success: false with an error
|
|
758
|
-
return { success: false, error: 'Compaction service unavailable' };
|
|
759
|
-
}
|
|
760
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
const result = await simulateAutoCompaction({
|
|
764
|
-
httpRequestStub,
|
|
765
|
-
verbose: false, // Non-verbose mode
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
// Should always show compaction failure message even without verbose
|
|
769
|
-
if (!result.logs.some((l) => l.includes('Auto-compaction failed'))) {
|
|
770
|
-
throw new Error(`should show compaction failure message even in non-verbose mode, got: ${result.logs.join(', ')}`);
|
|
771
|
-
}
|
|
772
|
-
if (!result.logs.some((l) => l.includes('Compaction service unavailable'))) {
|
|
773
|
-
throw new Error(`should include error message, got: ${result.logs.join(', ')}`);
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
// Test: Non-verbose mode - invalid newSessionID warning should always be shown
|
|
778
|
-
await runAsyncTest('Integration: should always show invalid newSessionID warning even when verbose is false', async () => {
|
|
779
|
-
const httpRequestStub = async (url, method, body) => {
|
|
780
|
-
if (url.includes('/compaction/check')) {
|
|
781
|
-
return { needed: true, totalTokens: 150000, usage: { limits: { threshold: 100000 } } };
|
|
782
|
-
}
|
|
783
|
-
if (url.includes('/compaction/compact')) {
|
|
784
|
-
// Server returns success but with null/invalid newSessionID
|
|
785
|
-
return { success: true, newSessionID: null };
|
|
786
|
-
}
|
|
787
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
788
|
-
};
|
|
789
|
-
|
|
790
|
-
const result = await simulateAutoCompaction({
|
|
791
|
-
httpRequestStub,
|
|
792
|
-
verbose: false, // Non-verbose mode
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Should always show invalid session warning even without verbose
|
|
796
|
-
if (!result.logs.some((l) => l.includes('invalid newSessionID'))) {
|
|
797
|
-
throw new Error(`should show invalid newSessionID warning even in non-verbose mode, got: ${result.logs.join(', ')}`);
|
|
798
|
-
}
|
|
799
|
-
// Should keep original session
|
|
800
|
-
assertEqual(result.sessionId, 'test-session-123', 'should not change session when newSessionID is invalid');
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// Test: Non-verbose mode - empty string newSessionID should also trigger warning
|
|
804
|
-
await runAsyncTest('Integration: should always show warning for empty string newSessionID when verbose is false', async () => {
|
|
805
|
-
const httpRequestStub = async (url, method, body) => {
|
|
806
|
-
if (url.includes('/compaction/check')) {
|
|
807
|
-
return { needed: true, totalTokens: 150000, usage: { limits: { threshold: 100000 } } };
|
|
808
|
-
}
|
|
809
|
-
if (url.includes('/compaction/compact')) {
|
|
810
|
-
// Server returns success but with empty string newSessionID
|
|
811
|
-
return { success: true, newSessionID: ' ' };
|
|
812
|
-
}
|
|
813
|
-
throw new Error(`Unexpected API call: ${url}`);
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
const result = await simulateAutoCompaction({
|
|
817
|
-
httpRequestStub,
|
|
818
|
-
verbose: false, // Non-verbose mode
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
// Should always show invalid session warning even without verbose
|
|
822
|
-
if (!result.logs.some((l) => l.includes('invalid newSessionID'))) {
|
|
823
|
-
throw new Error(`should show invalid newSessionID warning for empty string even in non-verbose mode, got: ${result.logs.join(', ')}`);
|
|
824
|
-
}
|
|
825
|
-
// Should keep original session
|
|
826
|
-
assertEqual(result.sessionId, 'test-session-123', 'should not change session when newSessionID is empty string');
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Run sync tests first, then async tests with proper awaiting
|
|
831
|
-
console.log('\n✓ Auto-compaction unit tests completed');
|
|
832
|
-
|
|
833
|
-
// ============================================================================
|
|
834
|
-
// Env var precedence tests (only run when lib/compaction.js is available)
|
|
835
|
-
// ============================================================================
|
|
836
|
-
try {
|
|
837
|
-
const compaction = require('../lib/compaction');
|
|
838
|
-
const { getDefaultModelIDForCompaction, getDefaultProviderIDForCompaction } = compaction;
|
|
839
|
-
|
|
840
|
-
console.log('\n=== Env Var Precedence Tests ===');
|
|
841
|
-
|
|
842
|
-
// Save original env vars
|
|
843
|
-
const origPaveModel = process.env.PAVE_MODEL;
|
|
844
|
-
const origPaveCompactModel = process.env.PAVE_COMPACT_MODEL;
|
|
845
|
-
|
|
846
|
-
function cleanTestEnv() {
|
|
847
|
-
delete process.env.PAVE_MODEL;
|
|
848
|
-
delete process.env.PAVE_COMPACT_MODEL;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function restoreTestEnv() {
|
|
852
|
-
if (origPaveModel !== undefined) process.env.PAVE_MODEL = origPaveModel;
|
|
853
|
-
else delete process.env.PAVE_MODEL;
|
|
854
|
-
if (origPaveCompactModel !== undefined) process.env.PAVE_COMPACT_MODEL = origPaveCompactModel;
|
|
855
|
-
else delete process.env.PAVE_COMPACT_MODEL;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Check if process.env mutation works in this environment
|
|
859
|
-
let canMutateEnv = false;
|
|
860
|
-
try {
|
|
861
|
-
cleanTestEnv();
|
|
862
|
-
process.env.__PAVE_TEST_PROBE = 'yes';
|
|
863
|
-
if (process.env.__PAVE_TEST_PROBE === 'yes') {
|
|
864
|
-
canMutateEnv = true;
|
|
865
|
-
}
|
|
866
|
-
delete process.env.__PAVE_TEST_PROBE;
|
|
867
|
-
} catch (e) {
|
|
868
|
-
canMutateEnv = false;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Test: no env vars -> hardcoded defaults (always works)
|
|
872
|
-
cleanTestEnv();
|
|
873
|
-
runTest('Env: no env vars -> hardcoded default model', () => {
|
|
874
|
-
assertEqual(getDefaultModelIDForCompaction(), 'claude-opus-4.5', 'default model');
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
cleanTestEnv();
|
|
878
|
-
runTest('Env: no env vars -> hardcoded default provider', () => {
|
|
879
|
-
assertEqual(getDefaultProviderIDForCompaction(), 'github-copilot', 'default provider');
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
try {
|
|
883
|
-
if (canMutateEnv) {
|
|
884
|
-
// Test: PAVE_MODEL sets default
|
|
885
|
-
cleanTestEnv();
|
|
886
|
-
process.env.PAVE_MODEL = 'github-copilot/claude-sonnet-4.6';
|
|
887
|
-
runTest('Env: PAVE_MODEL sets model', () => {
|
|
888
|
-
assertEqual(getDefaultModelIDForCompaction(), 'claude-sonnet-4.6', 'model from PAVE_MODEL');
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
cleanTestEnv();
|
|
892
|
-
process.env.PAVE_MODEL = 'github-copilot/claude-sonnet-4.6';
|
|
893
|
-
runTest('Env: PAVE_MODEL sets provider', () => {
|
|
894
|
-
assertEqual(getDefaultProviderIDForCompaction(), 'github-copilot', 'provider from PAVE_MODEL');
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Test: PAVE_COMPACT_MODEL takes precedence
|
|
898
|
-
cleanTestEnv();
|
|
899
|
-
process.env.PAVE_MODEL = 'github-copilot/claude-opus-4.6';
|
|
900
|
-
process.env.PAVE_COMPACT_MODEL = 'github-copilot/claude-sonnet-4';
|
|
901
|
-
runTest('Env: PAVE_COMPACT_MODEL takes precedence over PAVE_MODEL', () => {
|
|
902
|
-
assertEqual(getDefaultModelIDForCompaction(), 'claude-sonnet-4', 'compact model wins');
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// Test: bare model ID defaults to github-copilot
|
|
906
|
-
cleanTestEnv();
|
|
907
|
-
process.env.PAVE_COMPACT_MODEL = 'claude-sonnet-4.5';
|
|
908
|
-
runTest('Env: bare PAVE_COMPACT_MODEL uses default provider', () => {
|
|
909
|
-
assertEqual(getDefaultProviderIDForCompaction(), 'github-copilot', 'bare model -> default provider');
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// Test: malformed spec falls back
|
|
913
|
-
cleanTestEnv();
|
|
914
|
-
process.env.PAVE_COMPACT_MODEL = 'provider/';
|
|
915
|
-
runTest('Env: malformed PAVE_COMPACT_MODEL falls back to hardcoded', () => {
|
|
916
|
-
assertEqual(getDefaultModelIDForCompaction(), 'claude-opus-4.5', 'malformed -> hardcoded');
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
cleanTestEnv();
|
|
920
|
-
process.env.PAVE_COMPACT_MODEL = '/claude-opus-4.5';
|
|
921
|
-
runTest('Env: /model format falls back to default provider', () => {
|
|
922
|
-
assertEqual(getDefaultProviderIDForCompaction(), 'github-copilot', '/model -> default provider');
|
|
923
|
-
});
|
|
924
|
-
} else {
|
|
925
|
-
console.log(' (skipping env mutation tests - process.env is read-only in this environment)');
|
|
926
|
-
}
|
|
927
|
-
} finally {
|
|
928
|
-
restoreTestEnv();
|
|
929
|
-
}
|
|
930
|
-
console.log('✓ Env var precedence tests completed');
|
|
931
|
-
} catch (e) {
|
|
932
|
-
console.log('\n⚠️ Skipping env var precedence tests (lib/compaction.js not available in sandbox)');
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Run integration tests with proper await
|
|
936
|
-
runIntegrationTests().then(() => {
|
|
937
|
-
console.log('✓ Integration tests with stubbed httpRequest completed');
|
|
938
|
-
}).catch((err) => {
|
|
939
|
-
console.error('Integration tests failed:', err.message);
|
|
940
|
-
process.exitCode = 1;
|
|
941
|
-
});
|