@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,762 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Tests for post-stream idle verification fix (Issue #97)
|
|
4
|
-
*
|
|
5
|
-
* Verifies that after sendMessageAndStream returns, the agent loop
|
|
6
|
-
* calls waitForSessionIdle before sleeping, preventing "Session still busy"
|
|
7
|
-
* / HTTP 409 errors in the next iteration.
|
|
8
|
-
*
|
|
9
|
-
* Run with: node test/agent-post-stream-idle.test.js
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Test Utilities
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
function runTest(name, testFn) {
|
|
20
|
-
try {
|
|
21
|
-
testFn();
|
|
22
|
-
console.log(`\u2705 ${name}`);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.log(`\u274C ${name}: ${error.message}`);
|
|
25
|
-
process.exitCode = 1;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function assertEqual(actual, expected, message) {
|
|
30
|
-
if (actual !== expected) {
|
|
31
|
-
throw new Error(`${message}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function assertTrue(value, message) {
|
|
36
|
-
if (!value) {
|
|
37
|
-
throw new Error(`${message}: expected truthy value, got ${JSON.stringify(value)}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Structural Tests - Verify the fix exists in index.js
|
|
43
|
-
// ============================================================================
|
|
44
|
-
|
|
45
|
-
console.log('Running agent-post-stream-idle.test.js...');
|
|
46
|
-
console.log('\n=== Structural Tests: Post-Stream Idle Check ===');
|
|
47
|
-
|
|
48
|
-
const indexPath = path.join(__dirname, '..', 'index.js');
|
|
49
|
-
const indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
50
|
-
|
|
51
|
-
runTest('index.js should contain waitForSessionIdle function', () => {
|
|
52
|
-
assertTrue(
|
|
53
|
-
indexContent.includes('async function waitForSessionIdle('),
|
|
54
|
-
'waitForSessionIdle function should be defined',
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
runTest('waitForSessionIdle should return idle status and wait info', () => {
|
|
59
|
-
// Verify the function returns the expected shape using regex to tolerate
|
|
60
|
-
// formatting/whitespace changes (per Copilot review feedback)
|
|
61
|
-
const hasIdleTrueReturn = /return\s*{\s*idle\s*:\s*true\s*,\s*waited\s*,\s*waitTimeMs\s*}/.test(indexContent);
|
|
62
|
-
const hasIdleFalseReturn = /return\s*{\s*idle\s*:\s*false\s*,\s*waited\s*,\s*waitTimeMs\s*:/.test(indexContent);
|
|
63
|
-
|
|
64
|
-
assertTrue(
|
|
65
|
-
hasIdleTrueReturn,
|
|
66
|
-
'should return idle:true with waited and waitTimeMs',
|
|
67
|
-
);
|
|
68
|
-
assertTrue(
|
|
69
|
-
hasIdleFalseReturn,
|
|
70
|
-
'should return idle:false on timeout',
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
runTest('Agent loop should have post-stream idle check after sendMessageAndStream', () => {
|
|
75
|
-
// Assert on stable code structure rather than comment text:
|
|
76
|
-
// Verify waitForSessionIdle is called between sendMessageAndStream and the sleep log.
|
|
77
|
-
// Use handleAgentCommand as anchor to find the agent loop's send call (not the
|
|
78
|
-
// sendMessageAndStream function definition earlier in the file).
|
|
79
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
80
|
-
assertTrue(agentHandlerIdx >= 0, 'handleAgentCommand function should exist');
|
|
81
|
-
|
|
82
|
-
const sendIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
83
|
-
assertTrue(sendIdx > agentHandlerIdx, 'agent loop should await sendMessageAndStream');
|
|
84
|
-
|
|
85
|
-
const sleepIdx = indexContent.indexOf('Sleeping for', sendIdx);
|
|
86
|
-
assertTrue(sleepIdx > sendIdx, 'agent loop should log Sleeping for after sendMessageAndStream');
|
|
87
|
-
|
|
88
|
-
const idleIdx = indexContent.indexOf('waitForSessionIdle', sendIdx);
|
|
89
|
-
assertTrue(
|
|
90
|
-
idleIdx > sendIdx && idleIdx < sleepIdx,
|
|
91
|
-
'agent loop should call waitForSessionIdle between sendMessageAndStream and Sleeping for',
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
runTest('Post-stream idle check should use 30 second timeout (#271)', () => {
|
|
96
|
-
// Verify the waitForSessionIdle call between sendMessageAndStream and
|
|
97
|
-
// Sleeping for uses a 30 second (30 * 1000 ms) timeout — reduced from
|
|
98
|
-
// 10 minutes per issue #271 (HTTP 500 recovery).
|
|
99
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
100
|
-
const sendIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
101
|
-
assertTrue(sendIdx > agentHandlerIdx, 'agent loop should await sendMessageAndStream');
|
|
102
|
-
|
|
103
|
-
const sleepIdx = indexContent.indexOf('Sleeping for', sendIdx);
|
|
104
|
-
assertTrue(sleepIdx > sendIdx, 'Sleeping for marker should exist after sendMessageAndStream');
|
|
105
|
-
|
|
106
|
-
const idleIdx = indexContent.indexOf('waitForSessionIdle', sendIdx);
|
|
107
|
-
assertTrue(idleIdx > sendIdx && idleIdx < sleepIdx, 'waitForSessionIdle should exist between send and sleep');
|
|
108
|
-
|
|
109
|
-
const postStreamSection = indexContent.substring(idleIdx, sleepIdx);
|
|
110
|
-
assertTrue(
|
|
111
|
-
postStreamSection.includes('30 * 1000'),
|
|
112
|
-
'post-stream idle check should use 30 second timeout (reduced from 10 minutes per #271)',
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
runTest('Post-stream idle check should use 1000ms poll interval (#271)', () => {
|
|
117
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
118
|
-
const sendIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
119
|
-
assertTrue(sendIdx > agentHandlerIdx, 'agent loop should await sendMessageAndStream');
|
|
120
|
-
|
|
121
|
-
const sleepIdx = indexContent.indexOf('Sleeping for', sendIdx);
|
|
122
|
-
assertTrue(sleepIdx > sendIdx, 'Sleeping for marker should exist after sendMessageAndStream');
|
|
123
|
-
|
|
124
|
-
const idleIdx = indexContent.indexOf('waitForSessionIdle', sendIdx);
|
|
125
|
-
assertTrue(idleIdx > sendIdx && idleIdx < sleepIdx, 'waitForSessionIdle should exist between send and sleep');
|
|
126
|
-
|
|
127
|
-
const postStreamSection = indexContent.substring(idleIdx, sleepIdx);
|
|
128
|
-
assertTrue(
|
|
129
|
-
postStreamSection.includes('pollIntervalMs: 1000'),
|
|
130
|
-
'post-stream idle check should poll every 1000ms (increased frequency per #271)',
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
runTest('Post-stream idle check should log wait time when session was still processing', () => {
|
|
135
|
-
assertTrue(
|
|
136
|
-
indexContent.includes('Session was still processing after stream ended'),
|
|
137
|
-
'should log informative message when session was still processing after successful wait',
|
|
138
|
-
);
|
|
139
|
-
assertTrue(
|
|
140
|
-
indexContent.includes('Session did not become idle after stream ended'),
|
|
141
|
-
'should log warning when session timed out and did not become idle',
|
|
142
|
-
);
|
|
143
|
-
assertTrue(
|
|
144
|
-
indexContent.includes('Could not verify session idle state after stream ended'),
|
|
145
|
-
'should log distinct warning when status check failed with an error',
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
runTest('Post-stream idle check occurs BEFORE sleep, AFTER sendMessageAndStream in agent loop', () => {
|
|
150
|
-
// The postSendCheck variable is unique to the agent loop, so find it and
|
|
151
|
-
// verify it appears between sendMessageAndStream and signalAwareSleep
|
|
152
|
-
// Use the postSendCheck as anchor since it only exists in the agent handler
|
|
153
|
-
const postCheckIdx = indexContent.indexOf('postSendCheck');
|
|
154
|
-
assertTrue(postCheckIdx > 0, 'postSendCheck should exist');
|
|
155
|
-
|
|
156
|
-
// Find the sendMessageAndStream call that precedes postSendCheck
|
|
157
|
-
// (search backwards from postSendCheck)
|
|
158
|
-
const beforePostCheck = indexContent.substring(0, postCheckIdx);
|
|
159
|
-
const sendMsgIdx = beforePostCheck.lastIndexOf('await sendMessageAndStream(');
|
|
160
|
-
assertTrue(sendMsgIdx > 0, 'sendMessageAndStream should precede postSendCheck');
|
|
161
|
-
|
|
162
|
-
// Find the signalAwareSleep that follows postSendCheck
|
|
163
|
-
const sleepIdx = indexContent.indexOf('signalAwareSleep(', postCheckIdx);
|
|
164
|
-
assertTrue(sleepIdx > 0, 'signalAwareSleep should follow postSendCheck');
|
|
165
|
-
|
|
166
|
-
assertTrue(
|
|
167
|
-
sendMsgIdx < postCheckIdx && postCheckIdx < sleepIdx,
|
|
168
|
-
'order should be: sendMessageAndStream -> postSendCheck -> signalAwareSleep',
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
runTest('Post-stream idle check is gated to skip only HTTP 409 rejections', () => {
|
|
173
|
-
// The post-stream idle check should run for all cases EXCEPT when
|
|
174
|
-
// sendMessageAndStream threw with HTTP 409 (session busy), since the
|
|
175
|
-
// server definitively rejected the request. For non-409 errors (e.g.,
|
|
176
|
-
// SSE connection drops), the server may still be processing.
|
|
177
|
-
assertTrue(
|
|
178
|
-
indexContent.includes('let sendRejectedAs409 = false'),
|
|
179
|
-
'sendRejectedAs409 flag should be declared as false before try block',
|
|
180
|
-
);
|
|
181
|
-
assertTrue(
|
|
182
|
-
indexContent.includes('sendRejectedAs409 = true'),
|
|
183
|
-
'sendRejectedAs409 should be set to true in the 409 catch branch',
|
|
184
|
-
);
|
|
185
|
-
assertTrue(
|
|
186
|
-
indexContent.includes('if (!sendRejectedAs409)'),
|
|
187
|
-
'post-stream idle check should be gated by !sendRejectedAs409 flag',
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Verify sendRejectedAs409 = true is inside the catch block (409 handling)
|
|
191
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
192
|
-
const sendMsgIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
193
|
-
const catchIdx = indexContent.indexOf('} catch (err)', sendMsgIdx);
|
|
194
|
-
const rejectedIdx = indexContent.indexOf('sendRejectedAs409 = true', catchIdx);
|
|
195
|
-
assertTrue(rejectedIdx > catchIdx, 'sendRejectedAs409=true should be inside the catch block');
|
|
196
|
-
|
|
197
|
-
// Verify if (!sendRejectedAs409) comes AFTER the catch block
|
|
198
|
-
const ifNotRejectedIdx = indexContent.indexOf('if (!sendRejectedAs409)', catchIdx);
|
|
199
|
-
assertTrue(ifNotRejectedIdx > catchIdx, 'if (!sendRejectedAs409) gate should come after the catch block');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
runTest('Pre-send idle check should still exist (belt-and-suspenders)', () => {
|
|
203
|
-
// Locate the pre-send waitForSessionIdle call in the agent handler.
|
|
204
|
-
// It should appear before the main sendMessageAndStream call in the loop.
|
|
205
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
206
|
-
assertTrue(agentHandlerIdx >= 0, 'handleAgentCommand function should exist');
|
|
207
|
-
|
|
208
|
-
const sendMsgIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
209
|
-
assertTrue(sendMsgIdx > agentHandlerIdx, 'sendMessageAndStream should exist in agent handler');
|
|
210
|
-
|
|
211
|
-
// The pre-send waitForSessionIdle call should appear between handleAgentCommand and sendMessageAndStream
|
|
212
|
-
const preSendSection = indexContent.substring(agentHandlerIdx, sendMsgIdx);
|
|
213
|
-
assertTrue(
|
|
214
|
-
preSendSection.includes('waitForSessionIdle'),
|
|
215
|
-
'pre-send waitForSessionIdle call should exist before sendMessageAndStream',
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
runTest('Pre-send uses shorter timeout (5 min) vs post-stream (10 min)', () => {
|
|
220
|
-
// Find the pre-send waitForSessionIdle call (the one before sendMessageAndStream)
|
|
221
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
222
|
-
const sendMsgIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
223
|
-
const preSendSection = indexContent.substring(agentHandlerIdx, sendMsgIdx);
|
|
224
|
-
|
|
225
|
-
assertTrue(
|
|
226
|
-
preSendSection.includes('5 * 60 * 1000'),
|
|
227
|
-
'pre-send check should use 5 minute timeout',
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ============================================================================
|
|
232
|
-
// waitForSessionIdle Unit Tests (functional simulation)
|
|
233
|
-
// ============================================================================
|
|
234
|
-
|
|
235
|
-
console.log('\n=== Functional Tests: waitForSessionIdle behavior ===');
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Simulates waitForSessionIdle with a stubbed httpRequest.
|
|
239
|
-
* Mirrors the actual function logic from index.js.
|
|
240
|
-
* Fully synchronous — uses simulated elapsed time instead of real timers.
|
|
241
|
-
*/
|
|
242
|
-
function simulateWaitForSessionIdle(statusSequence, options = {}) {
|
|
243
|
-
const { maxWaitMs = 5000, pollIntervalMs = 50, verbose = false } = options;
|
|
244
|
-
let simulatedElapsedMs = 0;
|
|
245
|
-
let waited = false;
|
|
246
|
-
let pollCount = 0;
|
|
247
|
-
const logs = [];
|
|
248
|
-
|
|
249
|
-
for (const status of statusSequence) {
|
|
250
|
-
pollCount++;
|
|
251
|
-
|
|
252
|
-
// Simulate an error during status check (mirrors the catch branch in production)
|
|
253
|
-
if (status && typeof status === 'object' && status.type === 'error') {
|
|
254
|
-
return { idle: true, waited, waitTimeMs: simulatedElapsedMs, error: status.message || 'Unknown error', pollCount, logs };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Simulate the idle check
|
|
258
|
-
if (!status || (typeof status === 'object' && status.type === 'idle')) {
|
|
259
|
-
return { idle: true, waited, waitTimeMs: simulatedElapsedMs, pollCount, logs };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Session is busy
|
|
263
|
-
if (!waited) {
|
|
264
|
-
waited = true;
|
|
265
|
-
if (verbose) {
|
|
266
|
-
logs.push('Session is busy, waiting for idle...');
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Simulate poll interval elapsed
|
|
271
|
-
simulatedElapsedMs += pollIntervalMs;
|
|
272
|
-
|
|
273
|
-
// Check timeout
|
|
274
|
-
if (simulatedElapsedMs >= maxWaitMs) {
|
|
275
|
-
return { idle: false, waited, waitTimeMs: simulatedElapsedMs, timedOut: true, pollCount, logs };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Exhausted status sequence without becoming idle
|
|
280
|
-
return { idle: false, waited, waitTimeMs: simulatedElapsedMs, pollCount, logs };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Simulates the agent loop's post-stream behavior.
|
|
285
|
-
* Shows that the idle check after sendMessageAndStream prevents
|
|
286
|
-
* sleeping while the session is still busy.
|
|
287
|
-
* Fully synchronous — no real timers needed.
|
|
288
|
-
*/
|
|
289
|
-
function simulateAgentLoopIteration(options = {}) {
|
|
290
|
-
const {
|
|
291
|
-
sendMessageSuccess = true,
|
|
292
|
-
sendThrew = false, // true = sendMessageAndStream threw
|
|
293
|
-
sendThrew409 = false, // true = threw specifically with 409
|
|
294
|
-
sessionStatusAfterSend = [null], // null = idle
|
|
295
|
-
verbose = false,
|
|
296
|
-
maxWaitMs = 5000,
|
|
297
|
-
pollIntervalMs = 50,
|
|
298
|
-
} = options;
|
|
299
|
-
|
|
300
|
-
const logs = [];
|
|
301
|
-
const actions = [];
|
|
302
|
-
let postSendCheck = null;
|
|
303
|
-
|
|
304
|
-
// Simulate sendMessageAndStream
|
|
305
|
-
actions.push('sendMessageAndStream');
|
|
306
|
-
let sendRejectedAs409 = false;
|
|
307
|
-
if (sendThrew) {
|
|
308
|
-
if (sendThrew409) {
|
|
309
|
-
// sendMessageAndStream threw with HTTP 409
|
|
310
|
-
sendRejectedAs409 = true;
|
|
311
|
-
logs.push('Error: Session is busy (HTTP 409)');
|
|
312
|
-
} else {
|
|
313
|
-
// sendMessageAndStream threw with non-409 error (e.g., SSE connection drop)
|
|
314
|
-
logs.push('Error: Connection reset');
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
if (!sendMessageSuccess) {
|
|
318
|
-
logs.push('Error in iteration');
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Post-stream idle check (the fix from this PR)
|
|
323
|
-
// Run for all cases EXCEPT 409 rejections, matching the
|
|
324
|
-
// `if (!sendRejectedAs409)` gate in production code.
|
|
325
|
-
if (!sendRejectedAs409) {
|
|
326
|
-
actions.push('postStreamIdleCheck');
|
|
327
|
-
postSendCheck = simulateWaitForSessionIdle(sessionStatusAfterSend, {
|
|
328
|
-
verbose,
|
|
329
|
-
maxWaitMs,
|
|
330
|
-
pollIntervalMs,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
if (postSendCheck.error) {
|
|
334
|
-
// Status check failed — assumed idle, but warn operator
|
|
335
|
-
logs.push(`Could not verify session idle state after stream ended: ${postSendCheck.error}`);
|
|
336
|
-
} else if (postSendCheck.waited) {
|
|
337
|
-
const waitedSeconds = Math.round(postSendCheck.waitTimeMs / 1000);
|
|
338
|
-
if (postSendCheck.idle) {
|
|
339
|
-
logs.push(`Session was still processing after stream ended, waited ${waitedSeconds}s for idle`);
|
|
340
|
-
} else {
|
|
341
|
-
const timedOutFlag = postSendCheck.timedOut ? ' (timed out waiting for idle)' : '';
|
|
342
|
-
logs.push(`Session did not become idle after stream ended, waited ${waitedSeconds}s${timedOutFlag}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Sleep
|
|
348
|
-
actions.push('sleep');
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
actions,
|
|
352
|
-
logs,
|
|
353
|
-
postSendCheck,
|
|
354
|
-
sendRejectedAs409,
|
|
355
|
-
sleptWhileBusy: postSendCheck ? (postSendCheck.waited && !postSendCheck.idle) : false,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// All simulations are fully synchronous — no async IIFE needed.
|
|
360
|
-
// This ensures the test runner can track pass/fail reliably.
|
|
361
|
-
|
|
362
|
-
runTest('Session already idle: should return immediately without waiting', () => {
|
|
363
|
-
const result = simulateWaitForSessionIdle([null]); // null = idle
|
|
364
|
-
assertEqual(result.idle, true, 'should report idle');
|
|
365
|
-
assertEqual(result.waited, false, 'should not have waited');
|
|
366
|
-
assertEqual(result.pollCount, 1, 'should poll only once');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
runTest('Session busy then idle: should wait and report', () => {
|
|
370
|
-
const result = simulateWaitForSessionIdle([
|
|
371
|
-
{ type: 'busy' },
|
|
372
|
-
{ type: 'busy' },
|
|
373
|
-
null, // becomes idle
|
|
374
|
-
], { verbose: true });
|
|
375
|
-
|
|
376
|
-
assertEqual(result.idle, true, 'should report idle');
|
|
377
|
-
assertEqual(result.waited, true, 'should have waited');
|
|
378
|
-
assertEqual(result.pollCount, 3, 'should poll 3 times');
|
|
379
|
-
assertTrue(result.logs.length > 0, 'should have logged wait message');
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
runTest('Session stays busy: should timeout', () => {
|
|
383
|
-
const result = simulateWaitForSessionIdle([
|
|
384
|
-
{ type: 'busy' },
|
|
385
|
-
{ type: 'busy' },
|
|
386
|
-
{ type: 'busy' },
|
|
387
|
-
{ type: 'busy' },
|
|
388
|
-
{ type: 'busy' },
|
|
389
|
-
], { maxWaitMs: 100, pollIntervalMs: 30 });
|
|
390
|
-
|
|
391
|
-
assertEqual(result.idle, false, 'should report not idle');
|
|
392
|
-
assertEqual(result.waited, true, 'should have waited');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
runTest('Agent loop: session idle after stream - no extra wait', () => {
|
|
396
|
-
const result = simulateAgentLoopIteration({
|
|
397
|
-
sessionStatusAfterSend: [null], // Immediately idle
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
assertEqual(result.postSendCheck.waited, false, 'should not have waited');
|
|
401
|
-
assertEqual(result.postSendCheck.idle, true, 'should be idle');
|
|
402
|
-
assertEqual(result.sleptWhileBusy, false, 'should not sleep while busy');
|
|
403
|
-
// Verify correct action order
|
|
404
|
-
assertEqual(result.actions[0], 'sendMessageAndStream', 'first action');
|
|
405
|
-
assertEqual(result.actions[1], 'postStreamIdleCheck', 'second action');
|
|
406
|
-
assertEqual(result.actions[2], 'sleep', 'third action');
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
runTest('Agent loop: session busy after stream - waits before sleep', () => {
|
|
410
|
-
const result = simulateAgentLoopIteration({
|
|
411
|
-
sessionStatusAfterSend: [
|
|
412
|
-
{ type: 'busy' }, // SSE stream ended but server still processing
|
|
413
|
-
{ type: 'busy' },
|
|
414
|
-
null, // Server finishes
|
|
415
|
-
],
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
assertEqual(result.postSendCheck.waited, true, 'should have waited');
|
|
419
|
-
assertEqual(result.postSendCheck.idle, true, 'should eventually become idle');
|
|
420
|
-
assertEqual(result.sleptWhileBusy, false, 'should not sleep while busy');
|
|
421
|
-
assertTrue(
|
|
422
|
-
result.logs.some((l) => l.includes('Session was still processing')),
|
|
423
|
-
'should log that session was still processing',
|
|
424
|
-
);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
runTest('Agent loop: session hangs - timeout prevents infinite block', () => {
|
|
428
|
-
const result = simulateAgentLoopIteration({
|
|
429
|
-
sessionStatusAfterSend: [
|
|
430
|
-
{ type: 'busy' },
|
|
431
|
-
{ type: 'busy' },
|
|
432
|
-
{ type: 'busy' },
|
|
433
|
-
{ type: 'busy' },
|
|
434
|
-
{ type: 'busy' },
|
|
435
|
-
],
|
|
436
|
-
maxWaitMs: 100,
|
|
437
|
-
pollIntervalMs: 30,
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
assertEqual(result.postSendCheck.waited, true, 'should have waited');
|
|
441
|
-
assertEqual(result.postSendCheck.idle, false, 'should report not idle after timeout');
|
|
442
|
-
// Still proceeds to sleep (timeout is a safety valve, not a blocker)
|
|
443
|
-
assertEqual(result.actions[2], 'sleep', 'should still proceed to sleep after timeout');
|
|
444
|
-
// Should produce a warning about not becoming idle
|
|
445
|
-
assertTrue(
|
|
446
|
-
result.logs.some((l) => l.includes('Session did not become idle')),
|
|
447
|
-
'should warn that session did not become idle',
|
|
448
|
-
);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
runTest('Agent loop: HTTP 409 rejection skips idle check', () => {
|
|
452
|
-
const result = simulateAgentLoopIteration({
|
|
453
|
-
sendThrew: true,
|
|
454
|
-
sendThrew409: true,
|
|
455
|
-
sessionStatusAfterSend: [{ type: 'busy' }, null],
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// When sendMessageAndStream threw with 409, the server definitively
|
|
459
|
-
// rejected our request, so the post-stream idle check is unnecessary.
|
|
460
|
-
assertEqual(result.sendRejectedAs409, true, 'sendRejectedAs409 should be true');
|
|
461
|
-
assertTrue(
|
|
462
|
-
!result.actions.includes('postStreamIdleCheck'),
|
|
463
|
-
'should NOT perform post-stream idle check when 409 rejected',
|
|
464
|
-
);
|
|
465
|
-
assertEqual(result.postSendCheck, null, 'postSendCheck should be null when skipped');
|
|
466
|
-
// Should still proceed to sleep
|
|
467
|
-
assertTrue(
|
|
468
|
-
result.actions.includes('sleep'),
|
|
469
|
-
'should still proceed to sleep after 409 rejection',
|
|
470
|
-
);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
runTest('Agent loop: non-409 throw still gets idle check', () => {
|
|
474
|
-
// When sendMessageAndStream threw with a non-409 error (e.g., SSE connection
|
|
475
|
-
// drop after POST accepted), the server may still be processing, so we
|
|
476
|
-
// should still check idle.
|
|
477
|
-
const result = simulateAgentLoopIteration({
|
|
478
|
-
sendThrew: true,
|
|
479
|
-
sendThrew409: false,
|
|
480
|
-
sessionStatusAfterSend: [{ type: 'busy' }, null],
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
assertEqual(result.sendRejectedAs409, false, 'sendRejectedAs409 should be false for non-409');
|
|
484
|
-
assertTrue(
|
|
485
|
-
result.actions.includes('postStreamIdleCheck'),
|
|
486
|
-
'should perform post-stream idle check for non-409 errors',
|
|
487
|
-
);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
runTest('Agent loop: sendMessageAndStream resolved with error still gets idle check', () => {
|
|
491
|
-
// When sendMessageAndStream resolves (even with response.success=false),
|
|
492
|
-
// the server may have partially processed the request, so we still check idle.
|
|
493
|
-
const result = simulateAgentLoopIteration({
|
|
494
|
-
sendMessageSuccess: false,
|
|
495
|
-
sendThrew: false,
|
|
496
|
-
sessionStatusAfterSend: [{ type: 'busy' }, null],
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
assertEqual(result.sendRejectedAs409, false, 'sendRejectedAs409 should be false when resolved');
|
|
500
|
-
assertTrue(
|
|
501
|
-
result.actions.includes('postStreamIdleCheck'),
|
|
502
|
-
'should still perform post-stream idle check when send resolved with error',
|
|
503
|
-
);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
runTest('Agent loop: status check error produces distinct warning', () => {
|
|
507
|
-
// When waitForSessionIdle fails to check status (e.g., server unreachable),
|
|
508
|
-
// it returns { idle: true, error: '...' }. The agent loop should produce
|
|
509
|
-
// a distinct diagnostic message rather than the normal "waited Xs for idle" log.
|
|
510
|
-
const result = simulateAgentLoopIteration({
|
|
511
|
-
sendMessageSuccess: true,
|
|
512
|
-
sessionStatusAfterSend: [{ type: 'error', message: 'ECONNREFUSED' }],
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
assertTrue(
|
|
516
|
-
result.actions.includes('postStreamIdleCheck'),
|
|
517
|
-
'should perform post-stream idle check',
|
|
518
|
-
);
|
|
519
|
-
assertEqual(result.postSendCheck.idle, true, 'error case assumes idle');
|
|
520
|
-
assertTrue(
|
|
521
|
-
typeof result.postSendCheck.error === 'string',
|
|
522
|
-
'should have an error field',
|
|
523
|
-
);
|
|
524
|
-
// The log should contain the distinct error warning, not the normal waited message
|
|
525
|
-
assertTrue(
|
|
526
|
-
result.logs.some((l) => { return l.includes('Could not verify session idle state'); }),
|
|
527
|
-
'should log distinct error warning',
|
|
528
|
-
);
|
|
529
|
-
assertTrue(
|
|
530
|
-
!result.logs.some((l) => { return l.includes('waited') && l.includes('for idle'); }),
|
|
531
|
-
'should NOT log the normal "waited for idle" message on error',
|
|
532
|
-
);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// ============================================================================
|
|
536
|
-
// SSE premature close scenario test
|
|
537
|
-
// ============================================================================
|
|
538
|
-
|
|
539
|
-
console.log('\n=== Scenario Tests: SSE premature close ===');
|
|
540
|
-
|
|
541
|
-
runTest('Scenario: SSE stream ends during long tool execution', () => {
|
|
542
|
-
// This simulates the exact bug from issue #97:
|
|
543
|
-
// 1. Agent sends message, SSE stream starts
|
|
544
|
-
// 2. Server starts executing a long tool (e.g., multi-file edit)
|
|
545
|
-
// 3. SSE connection drops (network issue on iSH)
|
|
546
|
-
// 4. sendMessageAndStream resolves (sseRes.on('end') calls finish(true))
|
|
547
|
-
// 5. WITHOUT FIX: agent sleeps, next iteration gets 409
|
|
548
|
-
// 6. WITH FIX: post-stream idle check catches that server is still busy
|
|
549
|
-
|
|
550
|
-
const result = simulateAgentLoopIteration({
|
|
551
|
-
sendMessageSuccess: true,
|
|
552
|
-
sessionStatusAfterSend: [
|
|
553
|
-
{ type: 'busy', elapsed: 5000 }, // Server still running tool
|
|
554
|
-
{ type: 'busy', elapsed: 10000 }, // Still processing...
|
|
555
|
-
{ type: 'busy', elapsed: 15000 }, // Almost done...
|
|
556
|
-
null, // Tool execution complete
|
|
557
|
-
],
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
// The post-stream check caught the still-busy session
|
|
561
|
-
assertEqual(result.postSendCheck.waited, true, 'should detect session was still busy');
|
|
562
|
-
assertEqual(result.postSendCheck.idle, true, 'should eventually see idle');
|
|
563
|
-
assertEqual(result.sleptWhileBusy, false, 'should NOT sleep while session is busy');
|
|
564
|
-
|
|
565
|
-
// Verify the fix logged that it caught a premature stream end
|
|
566
|
-
assertTrue(
|
|
567
|
-
result.logs.some((l) => l.includes('Session was still processing after stream ended')),
|
|
568
|
-
'should log that it caught the premature stream end',
|
|
569
|
-
);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
runTest('Scenario: SSE connection drop after POST accepted (non-409 throw)', () => {
|
|
573
|
-
// This scenario: POST was accepted, server started processing, but SSE
|
|
574
|
-
// connection dropped causing sendMessageAndStream to throw a non-409 error.
|
|
575
|
-
// The idle check should still run because the server may still be working.
|
|
576
|
-
const result = simulateAgentLoopIteration({
|
|
577
|
-
sendThrew: true,
|
|
578
|
-
sendThrew409: false,
|
|
579
|
-
sessionStatusAfterSend: [
|
|
580
|
-
{ type: 'busy' }, // Server still processing after connection drop
|
|
581
|
-
{ type: 'busy' },
|
|
582
|
-
null, // Server finishes
|
|
583
|
-
],
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
assertTrue(
|
|
587
|
-
result.actions.includes('postStreamIdleCheck'),
|
|
588
|
-
'should still check idle after non-409 connection error',
|
|
589
|
-
);
|
|
590
|
-
assertEqual(result.postSendCheck.waited, true, 'should have waited for idle');
|
|
591
|
-
assertEqual(result.postSendCheck.idle, true, 'should eventually become idle');
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
runTest('Scenario: Normal completion - no performance regression', () => {
|
|
595
|
-
// When everything works correctly, the post-stream check should be
|
|
596
|
-
// essentially free in terms of logic: one poll, session is already idle,
|
|
597
|
-
// and it returns immediately without extra waiting or logging.
|
|
598
|
-
const result = simulateAgentLoopIteration({
|
|
599
|
-
sendMessageSuccess: true,
|
|
600
|
-
sessionStatusAfterSend: [null], // Already idle (normal case)
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
assertEqual(result.postSendCheck.waited, false, 'should not wait when already idle');
|
|
604
|
-
assertEqual(result.postSendCheck.pollCount, 1, 'should only poll once');
|
|
605
|
-
assertEqual(result.logs.length, 0, 'should produce no extra log output in normal case');
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
// ============================================================================
|
|
609
|
-
// SSE Timeout & Cleanup Tests (Issue #189/#190)
|
|
610
|
-
// ============================================================================
|
|
611
|
-
|
|
612
|
-
console.log('\n=== SSE Timeout & Cleanup Tests ===');
|
|
613
|
-
|
|
614
|
-
runTest('SSE idle timeout value is set to 5 minutes', () => {
|
|
615
|
-
assertTrue(indexContent.includes('SSE_IDLE_TIMEOUT_MS = 5 * 60 * 1000'), 'Should configure SSE_IDLE_TIMEOUT_MS as 5 minutes');
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
runTest('SSE timeout handle is stored in a variable', () => {
|
|
619
|
-
assertTrue(
|
|
620
|
-
indexContent.includes('sseTimeout = setTimeout'),
|
|
621
|
-
'SSE timeout handle should be stored in sseTimeout variable',
|
|
622
|
-
);
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
runTest('SSE timeout is cleared in finish()', () => {
|
|
626
|
-
assertTrue(
|
|
627
|
-
indexContent.includes('clearTimeout(sseTimeout)'),
|
|
628
|
-
'finish() should clear the SSE timeout',
|
|
629
|
-
);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
runTest('SSE timeout is unref\'d to not keep process alive', () => {
|
|
633
|
-
assertTrue(
|
|
634
|
-
indexContent.includes('sseTimeout.unref'),
|
|
635
|
-
'SSE timeout should be unref\'d',
|
|
636
|
-
);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
runTest('SSE timeout rejects with error (not silent success)', () => {
|
|
640
|
-
assertTrue(
|
|
641
|
-
indexContent.includes('rejectWithCleanup') && indexContent.includes('safety timeout'),
|
|
642
|
-
'SSE timeout should reject with a descriptive timeout error',
|
|
643
|
-
);
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
runTest('resetSseTimeout helper exists and is called from processEvent (issue #216)', () => {
|
|
647
|
-
assertTrue(
|
|
648
|
-
indexContent.includes('const resetSseTimeout'),
|
|
649
|
-
'Should have resetSseTimeout helper function',
|
|
650
|
-
);
|
|
651
|
-
// processEvent should call resetSseTimeout to reset on every SSE event
|
|
652
|
-
const processEventIdx = indexContent.indexOf('const processEvent');
|
|
653
|
-
assertTrue(processEventIdx > -1, 'Should find processEvent');
|
|
654
|
-
const processEventBody = indexContent.substring(processEventIdx, processEventIdx + 500);
|
|
655
|
-
assertTrue(
|
|
656
|
-
processEventBody.includes('resetSseTimeout()'),
|
|
657
|
-
'processEvent should call resetSseTimeout() to reset idle timer on every SSE event',
|
|
658
|
-
);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
runTest('Initial SSE timeout uses resetSseTimeout (not inline setTimeout)', () => {
|
|
662
|
-
// After sseReq.end(), the initial timeout should use the shared resetSseTimeout()
|
|
663
|
-
const reqEndIdx = indexContent.indexOf('sseReq.end()');
|
|
664
|
-
assertTrue(reqEndIdx > -1, 'Should find sseReq.end()');
|
|
665
|
-
const afterReqEnd = indexContent.substring(reqEndIdx, reqEndIdx + 500);
|
|
666
|
-
assertTrue(
|
|
667
|
-
afterReqEnd.includes('resetSseTimeout()'),
|
|
668
|
-
'Initial timeout after sseReq.end() should use resetSseTimeout()',
|
|
669
|
-
);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
runTest('Error paths use rejectWithCleanup to clear timeout', () => {
|
|
673
|
-
assertTrue(
|
|
674
|
-
indexContent.includes('rejectWithCleanup'),
|
|
675
|
-
'Should have rejectWithCleanup helper',
|
|
676
|
-
);
|
|
677
|
-
// Find the sendMessageAndStream function body
|
|
678
|
-
const fnIdx = indexContent.indexOf('function sendMessageAndStream');
|
|
679
|
-
assertTrue(fnIdx > -1, 'Should find sendMessageAndStream');
|
|
680
|
-
const fnBody = indexContent.substring(fnIdx, fnIdx + 8000);
|
|
681
|
-
// Remove the rejectWithCleanup definition itself (which contains reject(err))
|
|
682
|
-
const withoutDef = fnBody.replace(/const rejectWithCleanup[\s\S]*?};/, '');
|
|
683
|
-
// Any remaining bare reject() calls would be a bug
|
|
684
|
-
const bareReject = withoutDef.match(/[^a-zA-Z]reject\s*\(/g);
|
|
685
|
-
assertTrue(
|
|
686
|
-
!bareReject,
|
|
687
|
-
'All reject calls should use rejectWithCleanup, found bare reject(): ' + (bareReject ? bareReject.length : 0),
|
|
688
|
-
);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
runTest('cleanup() destroys POST request to prevent socket leaks', () => {
|
|
692
|
-
// postReq should be tracked in outer scope and destroyed in cleanup()
|
|
693
|
-
const fnIdx = indexContent.indexOf('function sendMessageAndStream');
|
|
694
|
-
assertTrue(fnIdx > -1, 'Should find sendMessageAndStream');
|
|
695
|
-
const fnBody = indexContent.substring(fnIdx, fnIdx + 12000);
|
|
696
|
-
// postReq should be declared in outer scope (let postReq)
|
|
697
|
-
assertTrue(
|
|
698
|
-
fnBody.includes('let postReq'),
|
|
699
|
-
'postReq should be declared in outer scope with let',
|
|
700
|
-
);
|
|
701
|
-
// cleanup should destroy postReq
|
|
702
|
-
const cleanupIdx = fnBody.indexOf('const cleanup');
|
|
703
|
-
assertTrue(cleanupIdx > -1, 'Should find cleanup function');
|
|
704
|
-
const cleanupBody = fnBody.substring(cleanupIdx, cleanupIdx + 400);
|
|
705
|
-
assertTrue(
|
|
706
|
-
cleanupBody.includes('postReq') && cleanupBody.includes('destroy'),
|
|
707
|
-
'cleanup() should destroy postReq to prevent socket leaks',
|
|
708
|
-
);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
runTest('SSE cooldown runs in finally block (both success and error paths)', () => {
|
|
712
|
-
// The 500ms cooldown should be in a finally block so it runs on error paths too
|
|
713
|
-
const agentHandlerIdx = indexContent.indexOf('async function handleAgentCommand');
|
|
714
|
-
const sendMsgIdx = indexContent.indexOf('await sendMessageAndStream(', agentHandlerIdx);
|
|
715
|
-
assertTrue(sendMsgIdx > agentHandlerIdx, 'sendMessageAndStream should exist in agent handler');
|
|
716
|
-
// Find the catch and finally blocks near the sendMessageAndStream call
|
|
717
|
-
const catchIdx = indexContent.indexOf('catch (err)', sendMsgIdx);
|
|
718
|
-
assertTrue(catchIdx > sendMsgIdx, 'should have catch block after sendMessageAndStream');
|
|
719
|
-
const finallyIdx = indexContent.indexOf('finally {', catchIdx);
|
|
720
|
-
assertTrue(finallyIdx > catchIdx, 'should have finally block after catch');
|
|
721
|
-
// Use 800-char window to accommodate multi-line comments in the finally body
|
|
722
|
-
const finallyBody = indexContent.substring(finallyIdx, finallyIdx + 800);
|
|
723
|
-
assertTrue(
|
|
724
|
-
finallyBody.includes('setTimeout(resolve, 500)'),
|
|
725
|
-
'500ms cooldown should be inside finally block',
|
|
726
|
-
);
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
// ============================================================================
|
|
730
|
-
// handleChatCommand server cleanup (Issue #190 review)
|
|
731
|
-
// ============================================================================
|
|
732
|
-
|
|
733
|
-
console.log('\n=== handleChatCommand Server Cleanup Tests ===');
|
|
734
|
-
|
|
735
|
-
runTest('handleChatCommand closes server in finally block (not just success path)', () => {
|
|
736
|
-
const chatIdx = indexContent.indexOf('async function handleChatCommand');
|
|
737
|
-
assertTrue(chatIdx > -1, 'Should find handleChatCommand');
|
|
738
|
-
// Find the startServer call inside handleChatCommand
|
|
739
|
-
const startSrvIdx = indexContent.indexOf('startServer(', chatIdx);
|
|
740
|
-
assertTrue(startSrvIdx > chatIdx, 'handleChatCommand should call startServer');
|
|
741
|
-
// Find the finally block after the try/catch
|
|
742
|
-
const catchIdx = indexContent.indexOf('catch (err)', startSrvIdx);
|
|
743
|
-
assertTrue(catchIdx > startSrvIdx, 'should have catch block');
|
|
744
|
-
const finallyIdx = indexContent.indexOf('finally {', catchIdx);
|
|
745
|
-
assertTrue(finallyIdx > catchIdx, 'should have finally block after catch in handleChatCommand');
|
|
746
|
-
const finallyBody = indexContent.substring(finallyIdx, finallyIdx + 500);
|
|
747
|
-
assertTrue(
|
|
748
|
-
finallyBody.includes('server.close()'),
|
|
749
|
-
'finally block should close server to prevent listener leaks',
|
|
750
|
-
);
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// ============================================================================
|
|
754
|
-
// Final Summary
|
|
755
|
-
// ============================================================================
|
|
756
|
-
|
|
757
|
-
console.log('\n=== All Test Sections Complete ===');
|
|
758
|
-
if (process.exitCode === 1) {
|
|
759
|
-
console.log('Some tests failed.');
|
|
760
|
-
} else {
|
|
761
|
-
console.log('All tests passed!');
|
|
762
|
-
}
|