@cnrai/pave 0.3.32 → 0.3.34

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.
Files changed (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,854 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for HTTP 500 back-off and circuit-breaker (Issue #256)
4
+ *
5
+ * Verifies:
6
+ * - consecutiveFailures counter exists and is tracked
7
+ * - MAX_CONSECUTIVE_FAILURES constant defined
8
+ * - Back-off tiers: 1-2 normal, 3-5 = 2x, 6+ = 3x (capped)
9
+ * - Circuit-breaker exits loop after max failures
10
+ * - writeStatus includes lastError and consecutiveFailures
11
+ * - Error state in STATES enum
12
+ * - 409 errors not counted as consecutive failures
13
+ * - Success resets consecutiveFailures to 0
14
+ *
15
+ * Designed to run standalone: node test/http500-backoff.test.js
16
+ */
17
+
18
+ const path = require('path');
19
+ const fs = require('fs');
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+
24
+ function assert(condition, msg) {
25
+ if (!condition) throw new Error('Assertion failed: ' + msg);
26
+ }
27
+
28
+ function runTest(name, fn) {
29
+ try {
30
+ fn();
31
+ console.log('\u2705 ' + name);
32
+ passed++;
33
+ } catch (e) {
34
+ console.log('\u274C ' + name + ': ' + e.message);
35
+ failed++;
36
+ }
37
+ }
38
+
39
+ // Read source files for inspection
40
+ const pavePath = path.join(__dirname, '..', 'index.js');
41
+ const paveSource = fs.readFileSync(pavePath, 'utf8');
42
+
43
+ const registryPath = path.join(__dirname, '..', 'lib', 'agent-registry.js');
44
+ const registrySource = fs.readFileSync(registryPath, 'utf8');
45
+
46
+ // ============================================================
47
+ // 1. STATES enum has ERROR state
48
+ // ============================================================
49
+
50
+ runTest('STATES enum includes ERROR state', () => {
51
+ assert(registrySource.indexOf("ERROR: 'error'") !== -1,
52
+ 'should have ERROR state in STATES enum');
53
+ });
54
+
55
+ runTest('STATES enum includes required states for this feature', () => {
56
+ const requiredStates = ['ERROR', 'SLEEPING', 'WORKING', 'STOPPED'];
57
+ for (let i = 0; i < requiredStates.length; i++) {
58
+ assert(registrySource.indexOf(requiredStates[i] + ':') !== -1,
59
+ 'should have ' + requiredStates[i] + ' in STATES enum');
60
+ }
61
+ });
62
+
63
+ // ============================================================
64
+ // 2. Consecutive failure tracking variables
65
+ // ============================================================
66
+
67
+ runTest('consecutiveFailures counter declared and initialized to 0', () => {
68
+ assert(paveSource.indexOf('let consecutiveFailures = 0') !== -1,
69
+ 'should declare consecutiveFailures initialized to 0');
70
+ });
71
+
72
+ runTest('MAX_CONSECUTIVE_FAILURES constant set to 10', () => {
73
+ assert(paveSource.indexOf('MAX_CONSECUTIVE_FAILURES = 10') !== -1,
74
+ 'should set MAX_CONSECUTIVE_FAILURES to 10');
75
+ });
76
+
77
+ runTest('MAX_BACKOFF_MS defined at 15 minutes', () => {
78
+ assert(paveSource.indexOf('MAX_BACKOFF_MS') !== -1,
79
+ 'should define MAX_BACKOFF_MS');
80
+ assert(paveSource.indexOf('15 * 60 * 1000') !== -1,
81
+ 'should cap backoff at 15 minutes');
82
+ });
83
+
84
+ runTest('lastError variable declared and initialized to null', () => {
85
+ assert(paveSource.indexOf('let lastError = null') !== -1,
86
+ 'should declare lastError initialized to null');
87
+ });
88
+
89
+ // ============================================================
90
+ // 3. Failure increment on error paths
91
+ // ============================================================
92
+
93
+ runTest('consecutiveFailures incremented on response.success=false', () => {
94
+ // With tiered error handling (#271), consecutiveFailures++ is inside
95
+ // applyTieredErrorHandling which is called from the !response.success path
96
+ // Use 'if (!response.success)' to avoid matching the JSDoc comment
97
+ const idx = paveSource.indexOf('if (!response.success)');
98
+ assert(idx !== -1, 'should have response.success check');
99
+ const section = paveSource.substring(idx, idx + 800);
100
+ // The path should call applyTieredErrorHandling (which internally increments for non-server errors)
101
+ assert(section.indexOf('applyTieredErrorHandling') !== -1,
102
+ 'should call applyTieredErrorHandling for tiered error classification');
103
+ });
104
+
105
+ runTest('lastError set on response.success=false', () => {
106
+ // Use 'if (!response.success)' to avoid matching JSDoc
107
+ const idx = paveSource.indexOf('if (!response.success)');
108
+ assert(idx !== -1, 'should have response.success check');
109
+ const section = paveSource.substring(idx, idx + 600);
110
+ // lastError is set either directly (body-too-large path) or via applyTieredErrorHandling
111
+ assert(section.indexOf('lastError = responseError') !== -1 || section.indexOf('applyTieredErrorHandling') !== -1,
112
+ 'should set lastError on failure (directly or via applyTieredErrorHandling)');
113
+ });
114
+
115
+ runTest('error log includes failure count on response failure', () => {
116
+ // With tiered handling, the error log is produced by applyTieredErrorHandling
117
+ // which includes 'consecutive failure' in its log messages
118
+ const funcIdx = paveSource.indexOf('function applyTieredErrorHandling');
119
+ assert(funcIdx !== -1, 'should have applyTieredErrorHandling function');
120
+ const funcSection = paveSource.substring(funcIdx, funcIdx + 1000);
121
+ assert(funcSection.indexOf('consecutive failure') !== -1,
122
+ 'applyTieredErrorHandling should show failure count');
123
+ assert(funcSection.indexOf('MAX_CONSECUTIVE_FAILURES') !== -1,
124
+ 'applyTieredErrorHandling should show max in error log');
125
+ });
126
+
127
+ runTest('consecutiveFailures incremented in catch block (non-409)', () => {
128
+ // With tiered error handling (#271), the catch block calls applyTieredErrorHandling
129
+ // instead of directly incrementing consecutiveFailures
130
+ const idx = paveSource.indexOf('Handle HTTP 409');
131
+ assert(idx !== -1, 'should find the catch block with 409 handling');
132
+ const section = paveSource.substring(idx, idx + 1200);
133
+ assert(section.indexOf('applyTieredErrorHandling') !== -1,
134
+ 'catch block should call applyTieredErrorHandling for tiered classification');
135
+ });
136
+
137
+ runTest('lastError set in catch block via applyTieredErrorHandling', () => {
138
+ // applyTieredErrorHandling sets lastError internally with appropriate prefix
139
+ const idx = paveSource.indexOf('Handle HTTP 409');
140
+ assert(idx !== -1, 'should find catch block');
141
+ const section = paveSource.substring(idx, idx + 1200);
142
+ assert(section.indexOf('applyTieredErrorHandling(err.message') !== -1,
143
+ 'should pass err.message to applyTieredErrorHandling');
144
+ });
145
+
146
+ runTest('catch block error log includes failure count', () => {
147
+ // The error log is now produced by applyTieredErrorHandling
148
+ const funcIdx = paveSource.indexOf('function applyTieredErrorHandling');
149
+ assert(funcIdx !== -1, 'should have applyTieredErrorHandling function');
150
+ const funcSection = paveSource.substring(funcIdx, funcIdx + 1000);
151
+ assert(funcSection.indexOf('consecutive failure') !== -1,
152
+ 'should show failure count in error log');
153
+ });
154
+
155
+ // ============================================================
156
+ // 4. 409 errors NOT counted as consecutive failures
157
+ // ============================================================
158
+
159
+ runTest('HTTP 409 does not increment consecutiveFailures', () => {
160
+ const idx = paveSource.indexOf("err.message.includes('409')");
161
+ assert(idx !== -1, 'should detect 409 errors');
162
+ const section = paveSource.substring(idx, idx + 400);
163
+ // After the 409 block there may be an `else if` (e.g., body-too-large) or `else`
164
+ const blockEnd = section.indexOf('} else');
165
+ assert(blockEnd !== -1, 'should have else clause after 409 handling');
166
+ const block409 = section.substring(0, blockEnd);
167
+ assert(block409.indexOf('consecutiveFailures++') === -1,
168
+ 'should NOT increment consecutiveFailures for 409 errors');
169
+ });
170
+
171
+ runTest('409 block has comment explaining why not counted', () => {
172
+ const idx = paveSource.indexOf("err.message.includes('409')");
173
+ assert(idx !== -1, 'should detect 409 errors');
174
+ const section = paveSource.substring(idx, idx + 300);
175
+ assert(section.indexOf('don\'t count') !== -1 || section.indexOf('not a provider error') !== -1,
176
+ 'should have comment explaining why 409 is not counted');
177
+ });
178
+
179
+ runTest('409 block sets lastError to descriptive message (not stale)', () => {
180
+ const idx = paveSource.indexOf("err.message.includes('409')");
181
+ assert(idx !== -1, 'should detect 409 errors');
182
+ const section = paveSource.substring(idx, idx + 400);
183
+ // After the 409 block there may be an `else if` (e.g., body-too-large) or `else`
184
+ const blockEnd = section.indexOf('} else');
185
+ assert(blockEnd !== -1, 'should have else clause');
186
+ const block409 = section.substring(0, blockEnd);
187
+ assert(block409.indexOf('lastError =') !== -1,
188
+ 'should set lastError in 409 block so status reflects current condition');
189
+ assert(block409.indexOf('409') !== -1,
190
+ 'lastError message should mention 409');
191
+ });
192
+
193
+ // ============================================================
194
+ // 5. Success resets consecutiveFailures
195
+ // ============================================================
196
+
197
+ runTest('consecutiveFailures reset to 0 on success', () => {
198
+ const idx = paveSource.indexOf('Send succeeded -- commit all deferred state updates');
199
+ assert(idx !== -1, 'should find success comment');
200
+ const section = paveSource.substring(idx, idx + 300);
201
+ assert(section.indexOf('consecutiveFailures = 0') !== -1,
202
+ 'should reset consecutiveFailures to 0 on success');
203
+ });
204
+
205
+ runTest('lastError reset to null on success', () => {
206
+ const idx = paveSource.indexOf('Send succeeded -- commit all deferred state updates');
207
+ assert(idx !== -1, 'should find success comment');
208
+ const section = paveSource.substring(idx, idx + 300);
209
+ assert(section.indexOf('lastError = null') !== -1,
210
+ 'should reset lastError to null on success');
211
+ });
212
+
213
+ // ============================================================
214
+ // 6. Back-off tiers
215
+ // ============================================================
216
+
217
+ runTest('effectiveSleepMs defaults to sleepMs (tier 1: failures 1-2)', () => {
218
+ assert(paveSource.indexOf('let effectiveSleepMs = sleepMs') !== -1,
219
+ 'should define effectiveSleepMs starting at sleepMs');
220
+ });
221
+
222
+ runTest('back-off tier 2: failures 3-5 use 2x sleepMs', () => {
223
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
224
+ assert(idx !== -1, 'should find effectiveSleepMs');
225
+ const section = paveSource.substring(idx, idx + 600);
226
+ assert(section.indexOf('consecutiveFailures >= 3') !== -1,
227
+ 'should check for >= 3 consecutive failures');
228
+ assert(section.indexOf('sleepMs * 2') !== -1,
229
+ 'should use 2x sleepMs for tier 2');
230
+ });
231
+
232
+ runTest('back-off tier 3: failures 6+ use 3x sleepMs', () => {
233
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
234
+ assert(idx !== -1, 'should find effectiveSleepMs');
235
+ const section = paveSource.substring(idx, idx + 600);
236
+ assert(section.indexOf('consecutiveFailures >= 6') !== -1,
237
+ 'should check for >= 6 consecutive failures');
238
+ assert(section.indexOf('sleepMs * 3') !== -1,
239
+ 'should use 3x sleepMs for tier 3');
240
+ });
241
+
242
+ runTest('back-off capped at MAX_BACKOFF_MS', () => {
243
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
244
+ assert(idx !== -1, 'should find effectiveSleepMs');
245
+ const section = paveSource.substring(idx, idx + 600);
246
+ // Both tier 2 and tier 3 should cap with Math.min
247
+ const tier2Idx = section.indexOf('sleepMs * 2');
248
+ const tier3Idx = section.indexOf('sleepMs * 3');
249
+ assert(tier2Idx !== -1 && tier3Idx !== -1, 'should have both tiers');
250
+ // MAX_BACKOFF_MS should appear in the section (used in Math.min)
251
+ assert(section.indexOf('MAX_BACKOFF_MS') !== -1,
252
+ 'should cap effectiveSleepMs at MAX_BACKOFF_MS');
253
+ });
254
+
255
+ runTest('tier 3 checked before tier 2 (higher tier first)', () => {
256
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
257
+ assert(idx !== -1, 'should find effectiveSleepMs');
258
+ const section = paveSource.substring(idx, idx + 400);
259
+ const tier3Idx = section.indexOf('consecutiveFailures >= 6');
260
+ const tier2Idx = section.indexOf('consecutiveFailures >= 3');
261
+ assert(tier3Idx < tier2Idx,
262
+ 'tier 3 (>= 6) should be checked before tier 2 (>= 3) in if/else chain');
263
+ });
264
+
265
+ // ============================================================
266
+ // 7. Back-off log messages
267
+ // ============================================================
268
+
269
+ runTest('back-off log only shows "Backing off" at tier 2+ (>= 3 failures)', () => {
270
+ const idx = paveSource.indexOf('Backing off');
271
+ assert(idx !== -1, 'should have "Backing off" log message');
272
+ // The condition should be >= 3, not > 0
273
+ const section = paveSource.substring(Math.max(0, idx - 200), idx + 50);
274
+ assert(section.indexOf('consecutiveFailures >= 3') !== -1,
275
+ 'Backing off log should only appear when consecutiveFailures >= 3');
276
+ });
277
+
278
+ runTest('failures 1-2 use "Retrying" message (not "Backing off")', () => {
279
+ const idx = paveSource.indexOf('Retrying after error');
280
+ assert(idx !== -1, 'should have "Retrying after error" message for failures 1-2');
281
+ const section = paveSource.substring(Math.max(0, idx - 200), idx + 50);
282
+ assert(section.indexOf('consecutiveFailures > 0') !== -1,
283
+ 'Retrying message should appear when consecutiveFailures > 0 (and < 3)');
284
+ });
285
+
286
+ runTest('normal sleep log used when no failures', () => {
287
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
288
+ assert(idx !== -1, 'should find effectiveSleepMs');
289
+ const section = paveSource.substring(idx, idx + 1500);
290
+ assert(section.indexOf('consecutiveFailures >= 3') !== -1,
291
+ 'should check for tier 2+ failures for Backing off');
292
+ assert(section.indexOf('consecutiveFailures > 0') !== -1,
293
+ 'should check for any failures for Retrying');
294
+ assert(section.indexOf('Sleeping for') !== -1,
295
+ 'should have normal sleeping log for zero-failure case');
296
+ });
297
+
298
+ // ============================================================
299
+ // 8. Circuit-breaker
300
+ // ============================================================
301
+
302
+ runTest('circuit-breaker checks MAX_CONSECUTIVE_FAILURES', () => {
303
+ assert(paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES') !== -1,
304
+ 'should check if consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
305
+ });
306
+
307
+ runTest('circuit-breaker writes ERROR state', () => {
308
+ const idx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
309
+ assert(idx !== -1, 'should find circuit-breaker check');
310
+ const section = paveSource.substring(idx, idx + 800);
311
+ assert(section.indexOf('STATES.ERROR') !== -1,
312
+ 'should write ERROR state');
313
+ });
314
+
315
+ runTest('circuit-breaker breaks out of loop', () => {
316
+ const idx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
317
+ assert(idx !== -1, 'should find circuit-breaker check');
318
+ const section = paveSource.substring(idx, idx + 1000);
319
+ assert(section.indexOf('break') !== -1,
320
+ 'should break out of the agent loop');
321
+ });
322
+
323
+ runTest('circuit-breaker ERROR status includes lastError', () => {
324
+ const idx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
325
+ assert(idx !== -1, 'should find circuit-breaker check');
326
+ const section = paveSource.substring(idx, idx + 1500);
327
+ // Match shorthand `lastError,` or explicit `lastError:`
328
+ assert(/\blastError[,:\s]/.test(section),
329
+ 'should include lastError in ERROR status');
330
+ });
331
+
332
+ runTest('circuit-breaker ERROR status includes consecutiveFailures', () => {
333
+ const idx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
334
+ assert(idx !== -1, 'should find circuit-breaker check');
335
+ const section = paveSource.substring(idx, idx + 1500);
336
+ // Match shorthand `consecutiveFailures,` or explicit `consecutiveFailures:`
337
+ assert(/\bconsecutiveFailures[,:\s]/.test(section),
338
+ 'should include consecutiveFailures in ERROR status');
339
+ });
340
+
341
+ runTest('circuit-breaker log message is descriptive', () => {
342
+ const idx = paveSource.indexOf('Agent stopping after');
343
+ assert(idx !== -1, 'should have clear "Agent stopping after" log message');
344
+ const section = paveSource.substring(idx, idx + 200);
345
+ assert(section.indexOf('consecutive failures') !== -1,
346
+ 'should mention consecutive failures in stopping message');
347
+ assert(section.indexOf('lastError') !== -1 || section.indexOf('Last error') !== -1,
348
+ 'should mention last error in stopping message');
349
+ });
350
+
351
+ // ============================================================
352
+ // 9. Sleeping writeStatus includes error fields
353
+ // ============================================================
354
+
355
+ runTest('sleeping writeStatus includes lastError field', () => {
356
+ // Find the sleeping writeStatus (the one with STATES.SLEEPING)
357
+ const idx = paveSource.indexOf('state: registry.STATES.SLEEPING');
358
+ assert(idx !== -1, 'should find SLEEPING writeStatus');
359
+ const section = paveSource.substring(idx, idx + 500);
360
+ // Match shorthand `lastError,` or explicit `lastError:`
361
+ assert(/\blastError[,:\s]/.test(section),
362
+ 'should include lastError in sleeping writeStatus');
363
+ });
364
+
365
+ runTest('sleeping writeStatus includes consecutiveFailures field', () => {
366
+ const idx = paveSource.indexOf('state: registry.STATES.SLEEPING');
367
+ assert(idx !== -1, 'should find SLEEPING writeStatus');
368
+ const section = paveSource.substring(idx, idx + 500);
369
+ // Match shorthand `consecutiveFailures,` or explicit `consecutiveFailures:`
370
+ assert(/\bconsecutiveFailures[,:\s]/.test(section),
371
+ 'should include consecutiveFailures in sleeping writeStatus');
372
+ });
373
+
374
+ runTest('sleeping writeStatus shows different currentTask for errors vs normal', () => {
375
+ // Use effectiveSleepMs as anchor since it's unique to the main loop's sleep section
376
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
377
+ assert(idx !== -1, 'should find effectiveSleepMs in main loop');
378
+ const section = paveSource.substring(idx, idx + 3000);
379
+ assert(section.indexOf('backing off after error') !== -1,
380
+ 'should show "backing off" task when there are 3+ failures');
381
+ assert(section.indexOf('retrying after error') !== -1,
382
+ 'should show "retrying" task when there are 1-2 failures');
383
+ assert(section.indexOf("'sleeping for '") !== -1,
384
+ 'should show normal sleeping task when no failures');
385
+ });
386
+
387
+ runTest('currentTask messaging aligns with backoff tiers', () => {
388
+ // Use effectiveSleepMs as anchor since it's unique to the main loop's sleep section
389
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
390
+ assert(idx !== -1, 'should find effectiveSleepMs in main loop');
391
+ const section = paveSource.substring(idx, idx + 3000);
392
+ // The "backing off" message should only appear for >= 3 failures
393
+ // (matching the tier logic that doubles sleep at 3+)
394
+ const backoffIdx = section.indexOf('backing off after error');
395
+ const retryIdx = section.indexOf('retrying after error');
396
+ assert(backoffIdx !== -1 && retryIdx !== -1,
397
+ 'both backing off and retrying messages should exist');
398
+ // "consecutiveFailures >= 3" should gate the "backing off" message
399
+ assert(section.indexOf('consecutiveFailures >= 3') !== -1,
400
+ 'backing off threshold should match the >= 3 tier boundary');
401
+ });
402
+
403
+ // ============================================================
404
+ // 10. signalAwareSleep uses effectiveSleepMs
405
+ // ============================================================
406
+
407
+ runTest('signalAwareSleep uses effectiveSleepMs', () => {
408
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
409
+ assert(idx !== -1, 'should find effectiveSleepMs');
410
+ const section = paveSource.substring(idx, idx + 5000);
411
+ assert(section.indexOf('signalAwareSleep(effectiveSleepMs') !== -1,
412
+ 'signalAwareSleep should use effectiveSleepMs, not raw sleepMs');
413
+ });
414
+
415
+ // ============================================================
416
+ // 11. Circuit-breaker is placed BEFORE back-off (order matters)
417
+ // ============================================================
418
+
419
+ runTest('circuit-breaker check comes before back-off calculation', () => {
420
+ const cbIdx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
421
+ const boIdx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
422
+ assert(cbIdx !== -1 && boIdx !== -1, 'both should exist');
423
+ assert(cbIdx < boIdx,
424
+ 'circuit-breaker should be checked before back-off is calculated');
425
+ });
426
+
427
+ // ============================================================
428
+ // 12. Copilot review fixes
429
+ // ============================================================
430
+
431
+ runTest('response failure uses response.error when available', () => {
432
+ // Use 'if (!response.success)' to avoid matching JSDoc
433
+ const idx = paveSource.indexOf('if (!response.success)');
434
+ assert(idx !== -1, 'should find response.success check');
435
+ const section = paveSource.substring(idx, idx + 400);
436
+ assert(section.indexOf('response.error') !== -1 || section.indexOf('response && response.error') !== -1,
437
+ 'should use response.error for actionable error message');
438
+ });
439
+
440
+ runTest('catch block passes err.message to applyTieredErrorHandling', () => {
441
+ const idx = paveSource.indexOf('Handle HTTP 409');
442
+ assert(idx !== -1, 'should find catch block comment');
443
+ const section = paveSource.substring(idx, idx + 1200);
444
+ // The catch block should delegate to applyTieredErrorHandling with err.message
445
+ assert(section.indexOf("applyTieredErrorHandling(err.message || 'Unknown error'") !== -1,
446
+ 'catch block should pass err.message to applyTieredErrorHandling');
447
+ });
448
+
449
+ runTest('normal exit path closes agent log stream', () => {
450
+ const idx = paveSource.indexOf('Cleanup on normal exit');
451
+ assert(idx !== -1, 'should find normal cleanup section');
452
+ const section = paveSource.substring(idx, idx + 1500);
453
+ assert(section.indexOf('closeAgentLogStream') !== -1,
454
+ 'should close agent log stream on normal exit (incl. circuit-breaker)');
455
+ });
456
+
457
+ runTest('normal exit path restores console overrides', () => {
458
+ const idx = paveSource.indexOf('Cleanup on normal exit');
459
+ assert(idx !== -1, 'should find normal cleanup section');
460
+ const section = paveSource.substring(idx, idx + 1500);
461
+ assert(section.indexOf('originalConsoleLog') !== -1,
462
+ 'should restore console.log on normal exit');
463
+ assert(section.indexOf('originalConsoleWarn') !== -1,
464
+ 'should restore console.warn on normal exit');
465
+ });
466
+
467
+ runTest('circuit-breaker exit uses trippedCircuitBreaker flag', () => {
468
+ const idx = paveSource.indexOf('Cleanup on normal exit');
469
+ assert(idx !== -1, 'should find normal cleanup section');
470
+ const section = paveSource.substring(idx, idx + 2000);
471
+ assert(section.indexOf('trippedCircuitBreaker') !== -1,
472
+ 'should use trippedCircuitBreaker flag (not failure count) for exit code');
473
+ assert(section.indexOf('? 1 : 0') !== -1,
474
+ 'should return 1 on circuit-breaker exit, 0 on normal exit');
475
+ });
476
+
477
+ runTest('trippedCircuitBreaker flag set before break', () => {
478
+ const idx = paveSource.indexOf('Agent stopping after');
479
+ assert(idx !== -1, 'should find circuit-breaker log');
480
+ const section = paveSource.substring(idx, idx + 2000);
481
+ const flagIdx = section.indexOf('trippedCircuitBreaker = true');
482
+ const breakIdx = section.indexOf('break;');
483
+ assert(flagIdx !== -1, 'should set trippedCircuitBreaker = true');
484
+ assert(breakIdx !== -1, 'should break');
485
+ assert(flagIdx < breakIdx,
486
+ 'trippedCircuitBreaker should be set before break');
487
+ });
488
+
489
+ // ============================================================
490
+ // 11. Normal exit writes STOPPED + releases lock (#256 round 3)
491
+ // ============================================================
492
+
493
+ runTest('normal exit writes STOPPED status (not circuit-breaker)', () => {
494
+ const idx = paveSource.indexOf('Cleanup on normal exit');
495
+ assert(idx !== -1, 'should find normal cleanup section');
496
+ const section = paveSource.substring(idx, idx + 2000);
497
+ assert(section.indexOf('STATES.STOPPED') !== -1,
498
+ 'should write STOPPED status on normal exit');
499
+ assert(section.indexOf('!trippedCircuitBreaker') !== -1,
500
+ 'should only write STOPPED when NOT circuit-breaker (ERROR already written)');
501
+ });
502
+
503
+ runTest('normal exit releases agent lock', () => {
504
+ const idx = paveSource.indexOf('Cleanup on normal exit');
505
+ assert(idx !== -1, 'should find normal cleanup section');
506
+ const section = paveSource.substring(idx, idx + 2000);
507
+ assert(section.indexOf('releaseAgent(agentName)') !== -1,
508
+ 'should call releaseAgent to release lock on any exit path');
509
+ });
510
+
511
+ runTest('cleanup mirrors SIGINT handler (status + lock)', () => {
512
+ const idx = paveSource.indexOf('Cleanup on normal exit');
513
+ assert(idx !== -1, 'should find normal cleanup section');
514
+ const section = paveSource.substring(idx, idx + 2000);
515
+ // Must have both writeStatus and releaseAgent
516
+ assert(section.indexOf('writeStatus') !== -1 && section.indexOf('releaseAgent') !== -1,
517
+ 'normal exit should mirror SIGINT cleanup: writeStatus + releaseAgent');
518
+ });
519
+
520
+ // ============================================================
521
+ // 12. ERROR state preserved by listAgents (#256 round 3)
522
+ // ============================================================
523
+
524
+ runTest('listAgents preserves ERROR state (not overwritten to CRASHED)', () => {
525
+ assert(registrySource.indexOf("status.state !== STATES.ERROR") !== -1,
526
+ 'listAgents should exclude ERROR from CRASHED override (dead PID with ERROR = intentional)');
527
+ });
528
+
529
+ runTest('listAgents comment explains terminal states', () => {
530
+ const idx = registrySource.indexOf('Detect crashed agents');
531
+ assert(idx !== -1, 'should find crashed detection section');
532
+ const section = registrySource.substring(idx, idx + 300);
533
+ assert(section.indexOf('terminal') !== -1 || section.indexOf('STOPPED') !== -1,
534
+ 'comment should explain why ERROR and STOPPED are excluded from CRASHED override');
535
+ });
536
+
537
+ // ============================================================
538
+ // 14. SIGINT handler preserves ERROR state (#256 round 5)
539
+ // ============================================================
540
+
541
+ runTest('SIGINT cleanup guards against overwriting circuit-breaker ERROR state', () => {
542
+ // The SIGINT/SIGTERM handler should check trippedCircuitBreaker
543
+ // before writing STOPPED status, to avoid overwriting ERROR
544
+ const idx = paveSource.indexOf('Handle Ctrl+C gracefully');
545
+ assert(idx !== -1, 'should find SIGINT handler section');
546
+ const section = paveSource.substring(idx, idx + 2000);
547
+ assert(section.indexOf('trippedCircuitBreaker') !== -1,
548
+ 'SIGINT handler should check trippedCircuitBreaker before writing STOPPED');
549
+ });
550
+
551
+ runTest('trippedCircuitBreaker declared at function scope (before cleanup)', () => {
552
+ // trippedCircuitBreaker must be declared BEFORE the cleanup handler
553
+ // so it's accessible from both the handler and the try block
554
+ const cbDecl = paveSource.indexOf('let trippedCircuitBreaker');
555
+ const cleanupDecl = paveSource.indexOf('const cleanup = async');
556
+ assert(cbDecl !== -1, 'should find trippedCircuitBreaker declaration');
557
+ assert(cleanupDecl !== -1, 'should find cleanup handler declaration');
558
+ assert(cbDecl < cleanupDecl,
559
+ 'trippedCircuitBreaker must be declared before cleanup handler to avoid ReferenceError');
560
+ });
561
+
562
+ // ============================================================
563
+ // 13. sendMessageAndStream returns error field on failure (#256 round 4)
564
+ // ============================================================
565
+
566
+ runTest('finish() accepts optional errorMsg parameter', () => {
567
+ const idx = paveSource.indexOf('function sendMessageAndStream');
568
+ assert(idx !== -1, 'should find sendMessageAndStream');
569
+ const section = paveSource.substring(idx, idx + 5000);
570
+ assert(section.indexOf('finish = (success = true, errorMsg = null)') !== -1,
571
+ 'finish should accept an optional errorMsg parameter');
572
+ });
573
+
574
+ runTest('finish() includes error field in resolved object', () => {
575
+ const idx = paveSource.indexOf('function sendMessageAndStream');
576
+ assert(idx !== -1, 'should find sendMessageAndStream');
577
+ const section = paveSource.substring(idx, idx + 5000);
578
+ assert(section.indexOf('errorMsg != null') !== -1,
579
+ 'finish should use != null check (not truthy) to include error field');
580
+ assert(section.indexOf('result.error = errorMsg') !== -1,
581
+ 'finish should include error in the resolved object when errorMsg is provided');
582
+ });
583
+
584
+ runTest('session.error passes error message to finish()', () => {
585
+ const idx = paveSource.indexOf('function sendMessageAndStream');
586
+ assert(idx !== -1, 'should find sendMessageAndStream');
587
+ const section = paveSource.substring(idx, idx + 15000);
588
+ assert(section.indexOf('finish(false, errorMsg)') !== -1,
589
+ 'session.error handler should pass the error message to finish()');
590
+ });
591
+
592
+ // ============================================================
593
+ // 18. Server close with timeout and signal cleanup
594
+ // ============================================================
595
+
596
+ runTest('normal exit awaits server.close with timeout', () => {
597
+ // Find the normal exit cleanup section (after the loop)
598
+ const idx = paveSource.indexOf('// Cleanup on normal exit');
599
+ assert(idx !== -1, 'should find normal exit cleanup section');
600
+ const section = paveSource.substring(idx, idx + 1000);
601
+ assert(section.indexOf('server.close(') !== -1,
602
+ 'normal exit should call server.close()');
603
+ assert(section.indexOf('setTimeout(resolve, 5000)') !== -1,
604
+ 'normal exit should have 5s timeout for server close');
605
+ assert(section.indexOf('await new Promise') !== -1,
606
+ 'normal exit should await server close completion');
607
+ });
608
+
609
+ runTest('normal exit removes SIGINT/SIGTERM listeners', () => {
610
+ const idx = paveSource.indexOf('// Cleanup on normal exit');
611
+ assert(idx !== -1, 'should find normal exit cleanup section');
612
+ const section = paveSource.substring(idx, idx + 1000);
613
+ assert(section.indexOf("removeListener('SIGINT', cleanup)") !== -1,
614
+ 'normal exit should remove SIGINT listener');
615
+ assert(section.indexOf("removeListener('SIGTERM', cleanup)") !== -1,
616
+ 'normal exit should remove SIGTERM listener');
617
+ });
618
+
619
+ runTest('error exit awaits server.close with timeout', () => {
620
+ // Find the catch block cleanup
621
+ const idx = paveSource.indexOf('// Close log stream on error exit path');
622
+ assert(idx !== -1, 'should find error exit cleanup section');
623
+ const section = paveSource.substring(idx, idx + 1500);
624
+ assert(section.indexOf('server.close(') !== -1,
625
+ 'error exit should call server.close()');
626
+ assert(section.indexOf('setTimeout(resolve, 5000)') !== -1,
627
+ 'error exit should have 5s timeout for server close');
628
+ assert(section.indexOf("removeListener('SIGINT', cleanup)") !== -1,
629
+ 'error exit should remove SIGINT listener');
630
+ });
631
+
632
+ // ============================================================
633
+ // Done
634
+ // ============================================================
635
+
636
+ // ============================================================
637
+ // 19. Tiered error handling (#271)
638
+ // ============================================================
639
+
640
+ runTest('classifyError function exists', () => {
641
+ assert(paveSource.indexOf('function classifyError(') !== -1,
642
+ 'should have classifyError function for tiered error handling');
643
+ });
644
+
645
+ runTest('applyTieredErrorHandling function exists', () => {
646
+ assert(paveSource.indexOf('function applyTieredErrorHandling(') !== -1,
647
+ 'should have applyTieredErrorHandling function shared by both error paths');
648
+ });
649
+
650
+ runTest('classifyError detects server errors (500/502/503)', () => {
651
+ const idx = paveSource.indexOf('function classifyError(');
652
+ assert(idx !== -1, 'should find classifyError');
653
+ const section = paveSource.substring(idx, idx + 500);
654
+ assert(section.indexOf('server-error') !== -1,
655
+ 'should return server-error category');
656
+ assert(section.indexOf('50[023]') !== -1 || (section.indexOf('500') !== -1 && section.indexOf('502') !== -1 && section.indexOf('503') !== -1),
657
+ 'should detect 500, 502, 503 status codes');
658
+ });
659
+
660
+ runTest('classifyError detects rate limiting (429)', () => {
661
+ const idx = paveSource.indexOf('function classifyError(');
662
+ assert(idx !== -1, 'should find classifyError');
663
+ const section = paveSource.substring(idx, idx + 500);
664
+ assert(section.indexOf('rate-limited') !== -1,
665
+ 'should return rate-limited category');
666
+ assert(section.indexOf('429') !== -1,
667
+ 'should detect 429 status code');
668
+ assert(section.indexOf('rate limit') !== -1,
669
+ 'should detect "rate limit" text');
670
+ });
671
+
672
+ runTest('classifyError detects auth errors (401/403)', () => {
673
+ const idx = paveSource.indexOf('function classifyError(');
674
+ assert(idx !== -1, 'should find classifyError');
675
+ const section = paveSource.substring(idx, idx + 800);
676
+ assert(section.indexOf('auth-error') !== -1,
677
+ 'should return auth-error category');
678
+ assert(section.indexOf('unauthorized') !== -1 || section.indexOf('forbidden') !== -1,
679
+ 'should detect unauthorized/forbidden text');
680
+ });
681
+
682
+ runTest('classifyError uses case-insensitive matching', () => {
683
+ const idx = paveSource.indexOf('function classifyError(');
684
+ assert(idx !== -1, 'should find classifyError');
685
+ const section = paveSource.substring(idx, idx + 500);
686
+ assert(section.indexOf('.toLowerCase()') !== -1,
687
+ 'should normalize error message to lowercase for case-insensitive matching');
688
+ });
689
+
690
+ runTest('server errors do NOT increment consecutiveFailures', () => {
691
+ const idx = paveSource.indexOf('function applyTieredErrorHandling(');
692
+ assert(idx !== -1, 'should find applyTieredErrorHandling');
693
+ const section = paveSource.substring(idx, idx + 1000);
694
+ // Find the server-error branch
695
+ const serverIdx = section.indexOf("category === 'server-error'");
696
+ assert(serverIdx !== -1, 'should check for server-error category');
697
+ // The server-error branch should NOT have consecutiveFailures++
698
+ const nextElse = section.indexOf('} else', serverIdx);
699
+ const serverBranch = section.substring(serverIdx, nextElse);
700
+ assert(serverBranch.indexOf('consecutiveFailures++') === -1,
701
+ 'server error branch should NOT increment consecutiveFailures');
702
+ });
703
+
704
+ runTest('non-server errors DO increment consecutiveFailures', () => {
705
+ const idx = paveSource.indexOf('function applyTieredErrorHandling(');
706
+ assert(idx !== -1, 'should find applyTieredErrorHandling');
707
+ const section = paveSource.substring(idx, idx + 1000);
708
+ // rate-limited, auth-error, and other should all increment
709
+ const matches = section.match(/consecutiveFailures\+\+/g);
710
+ assert(matches && matches.length >= 3,
711
+ 'rate-limited, auth-error, and other categories should all increment consecutiveFailures');
712
+ });
713
+
714
+ runTest('server error uses fixed 5-minute backoff', () => {
715
+ assert(paveSource.indexOf('SERVER_ERROR_BACKOFF_MS') !== -1,
716
+ 'should define SERVER_ERROR_BACKOFF_MS constant');
717
+ assert(paveSource.indexOf('5 * 60 * 1000') !== -1,
718
+ 'should have 5-minute constant');
719
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
720
+ assert(idx !== -1, 'should find effectiveSleepMs');
721
+ const section = paveSource.substring(idx, idx + 600);
722
+ assert(section.indexOf('isServerError') !== -1,
723
+ 'should check isServerError flag');
724
+ assert(section.indexOf('SERVER_ERROR_BACKOFF_MS') !== -1,
725
+ 'should use SERVER_ERROR_BACKOFF_MS for server error backoff');
726
+ });
727
+
728
+ runTest('server error backoff capped at MAX_BACKOFF_MS', () => {
729
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
730
+ assert(idx !== -1, 'should find effectiveSleepMs');
731
+ const section = paveSource.substring(idx, idx + 600);
732
+ const serverBranch = section.indexOf('isServerError');
733
+ assert(serverBranch !== -1, 'should check isServerError');
734
+ const branchSection = section.substring(serverBranch, serverBranch + 200);
735
+ assert(branchSection.indexOf('MAX_BACKOFF_MS') !== -1,
736
+ 'server error backoff should be capped at MAX_BACKOFF_MS');
737
+ });
738
+
739
+ runTest('sleepTask includes server error messaging', () => {
740
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
741
+ assert(idx !== -1, 'should find effectiveSleepMs');
742
+ const section = paveSource.substring(idx, idx + 3000);
743
+ assert(section.indexOf('server error - retrying') !== -1,
744
+ 'sleepTask should include server-error-specific message');
745
+ });
746
+
747
+ runTest('tiered handling applied to both error paths', () => {
748
+ // Count applyTieredErrorHandling calls  should be at least 2
749
+ // (one in !response.success path, one in catch path)
750
+ let count = 0;
751
+ let searchIdx = 0;
752
+ while (true) {
753
+ const found = paveSource.indexOf('applyTieredErrorHandling(', searchIdx);
754
+ if (found === -1) break;
755
+ count++;
756
+ searchIdx = found + 1;
757
+ }
758
+ assert(count >= 2,
759
+ 'applyTieredErrorHandling should be called from both !response.success and catch paths (found ' + count + ' calls)');
760
+ });
761
+
762
+ runTest('error-aware retry message when lastError is set', () => {
763
+ const _idx = paveSource.indexOf('lastError');
764
+ // Find the message selection section
765
+ const msgIdx = paveSource.indexOf('} else if (lastError)');
766
+ assert(msgIdx !== -1, 'should have lastError check in message selection');
767
+ const section = paveSource.substring(msgIdx, msgIdx + 500);
768
+ assert(section.indexOf('previous iteration ended with an error') !== -1,
769
+ 'should include error context in retry message');
770
+ });
771
+
772
+ runTest('error-aware message includes actual lastError value', () => {
773
+ const msgIdx = paveSource.indexOf('} else if (lastError)');
774
+ assert(msgIdx !== -1, 'should have lastError check in message selection');
775
+ const section = paveSource.substring(msgIdx, msgIdx + 500);
776
+ assert(section.indexOf('lastError') !== -1 && section.indexOf("(' + lastError + ')") !== -1 || section.indexOf('+ lastError +') !== -1,
777
+ 'retry message should include the actual error value for context');
778
+ });
779
+
780
+ runTest('post-send idle check uses reduced 30s timeout (#271)', () => {
781
+ // Find the post-send idle check by looking for the specific comment
782
+ const idx = paveSource.indexOf('verify the session is actually idle');
783
+ assert(idx !== -1, 'should find post-send idle check comment');
784
+ const section = paveSource.substring(idx, idx + 1200);
785
+ assert(section.indexOf('30 * 1000') !== -1,
786
+ 'should use 30-second timeout (reduced from 10 minutes per #271)');
787
+ assert(section.indexOf('pollIntervalMs: 1000') !== -1,
788
+ 'should use 1-second poll interval');
789
+ });
790
+
791
+ runTest('sleepTask is computed once and reused', () => {
792
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
793
+ assert(idx !== -1, 'should find effectiveSleepMs');
794
+ const section = paveSource.substring(idx, idx + 3500);
795
+ // sleepTask should be defined once and used for both agentContext and writeStatus
796
+ assert(section.indexOf('const sleepTask =') !== -1,
797
+ 'should define sleepTask as const');
798
+ assert(section.indexOf('agentContext.currentTask = sleepTask') !== -1,
799
+ 'should use sleepTask for agentContext');
800
+ assert(section.indexOf('currentTask: sleepTask') !== -1,
801
+ 'should use sleepTask for writeStatus (no duplication)');
802
+ });
803
+
804
+ runTest('lastErrorCategory variable declared and initialized to null', () => {
805
+ assert(paveSource.indexOf('let lastErrorCategory = null') !== -1,
806
+ 'should declare lastErrorCategory initialized to null');
807
+ });
808
+
809
+ runTest('applyTieredErrorHandling sets lastErrorCategory', () => {
810
+ const idx = paveSource.indexOf('function applyTieredErrorHandling(');
811
+ assert(idx !== -1, 'should find applyTieredErrorHandling');
812
+ const section = paveSource.substring(idx, idx + 1000);
813
+ assert(section.indexOf('lastErrorCategory = category') !== -1,
814
+ 'should set lastErrorCategory from classified category');
815
+ });
816
+
817
+ runTest('isServerError uses lastErrorCategory not string prefix', () => {
818
+ const idx = paveSource.indexOf('let effectiveSleepMs = sleepMs');
819
+ assert(idx !== -1, 'should find effectiveSleepMs');
820
+ const section = paveSource.substring(idx - 200, idx);
821
+ assert(section.indexOf("lastErrorCategory === 'server-error'") !== -1,
822
+ 'should use lastErrorCategory for server error check (not string prefix)');
823
+ assert(section.indexOf("lastError.startsWith('Server error:')") === -1,
824
+ 'should NOT use string prefix matching for server error detection');
825
+ });
826
+
827
+ runTest('lastErrorCategory reset on success', () => {
828
+ const idx = paveSource.indexOf('Send succeeded -- commit all deferred state updates');
829
+ assert(idx !== -1, 'should find success comment');
830
+ const section = paveSource.substring(idx, idx + 300);
831
+ assert(section.indexOf('lastErrorCategory = null') !== -1,
832
+ 'should reset lastErrorCategory to null on success');
833
+ });
834
+
835
+ runTest('applyTieredErrorHandling has no unused context parameter', () => {
836
+ const idx = paveSource.indexOf('function applyTieredErrorHandling(');
837
+ assert(idx !== -1, 'should find applyTieredErrorHandling');
838
+ const section = paveSource.substring(idx, idx + 100);
839
+ // Should only accept errorMsg, not context
840
+ assert(section.indexOf('context') === -1,
841
+ 'should not have unused context parameter');
842
+ });
843
+
844
+ // ============================================================
845
+ // Done
846
+ // ============================================================
847
+
848
+ console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
849
+ if (failed > 0) {
850
+ console.log(failed + ' test(s) FAILED');
851
+ process.exitCode = 1;
852
+ } else {
853
+ console.log('All tests passed!');
854
+ }