@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -218
  3. package/package.json +32 -35
  4. package/pave.js +3 -0
  5. package/sandbox/SandboxRunner.js +1 -0
  6. package/sandbox/pave-run.js +2 -0
  7. package/sandbox/permission.js +1 -0
  8. package/sandbox/utils/yaml.js +1 -0
  9. package/MARKETPLACE.md +0 -406
  10. package/build-binary.js +0 -591
  11. package/build-npm.js +0 -537
  12. package/build.js +0 -230
  13. package/check-binary.js +0 -26
  14. package/deploy.sh +0 -95
  15. package/index.js +0 -5776
  16. package/lib/agent-registry.js +0 -1037
  17. package/lib/args-parser.js +0 -837
  18. package/lib/blessed-widget-patched.js +0 -93
  19. package/lib/cli-markdown.js +0 -590
  20. package/lib/compaction.js +0 -153
  21. package/lib/duration.js +0 -94
  22. package/lib/hash.js +0 -22
  23. package/lib/marketplace.js +0 -866
  24. package/lib/memory-config.js +0 -166
  25. package/lib/skill-manager.js +0 -891
  26. package/lib/soul.js +0 -31
  27. package/lib/tool-output-formatter.js +0 -180
  28. package/start-pave.sh +0 -149
  29. package/status.js +0 -271
  30. package/test/abort-stream.test.js +0 -445
  31. package/test/agent-auto-compaction.test.js +0 -552
  32. package/test/agent-comm-abort.test.js +0 -95
  33. package/test/agent-comm.test.js +0 -598
  34. package/test/agent-inbox.test.js +0 -576
  35. package/test/agent-init.test.js +0 -264
  36. package/test/agent-interrupt.test.js +0 -314
  37. package/test/agent-lifecycle.test.js +0 -520
  38. package/test/agent-log-files.test.js +0 -349
  39. package/test/agent-mode.manual-test.js +0 -392
  40. package/test/agent-parsing.test.js +0 -228
  41. package/test/agent-post-stream-idle.test.js +0 -762
  42. package/test/agent-registry.test.js +0 -359
  43. package/test/agent-rm.test.js +0 -442
  44. package/test/agent-spawn.test.js +0 -933
  45. package/test/agent-status-api.test.js +0 -624
  46. package/test/agent-update.test.js +0 -435
  47. package/test/args-parser.test.js +0 -391
  48. package/test/auto-compaction-chat.manual-test.js +0 -227
  49. package/test/auto-compaction.test.js +0 -941
  50. package/test/build-config.test.js +0 -120
  51. package/test/build-npm.test.js +0 -388
  52. package/test/chat-command.test.js +0 -137
  53. package/test/chat-leading-lines.test.js +0 -159
  54. package/test/config-flag.test.js +0 -272
  55. package/test/cursor-drift.test.js +0 -135
  56. package/test/debug-require.js +0 -23
  57. package/test/dir-migration.test.js +0 -323
  58. package/test/duration.test.js +0 -229
  59. package/test/ghostty-term.test.js +0 -202
  60. package/test/http500-backoff.test.js +0 -854
  61. package/test/integration.test.js +0 -86
  62. package/test/memory-guard-env.test.js +0 -220
  63. package/test/pr233-fixes.test.js +0 -259
  64. package/test/run-agent-init.js +0 -297
  65. package/test/run-all.js +0 -64
  66. package/test/run-config-flag.js +0 -159
  67. package/test/run-cursor-drift.js +0 -82
  68. package/test/run-session-path.js +0 -154
  69. package/test/run-tests.js +0 -643
  70. package/test/sandbox-redirect.test.js +0 -202
  71. package/test/session-path.test.js +0 -132
  72. package/test/shebang-strip.test.js +0 -241
  73. package/test/soul-reinject.test.js +0 -1027
  74. package/test/soul-reread.test.js +0 -281
  75. package/test/tool-output-formatter.test.js +0 -486
  76. package/test/tool-output-gating.test.js +0 -143
  77. package/test/tool-states.test.js +0 -167
  78. package/test/tools-flag.test.js +0 -65
  79. package/test/tui-attach.test.js +0 -1255
  80. package/test/tui-compaction.test.js +0 -354
  81. package/test/tui-wrap.test.js +0 -568
  82. package/test-binary.js +0 -52
  83. package/test-binary2.js +0 -36
@@ -1,624 +0,0 @@
1
- /**
2
- * Tests for Agent Status API (Phase 2 of issue #107: pave agent is a server)
3
- *
4
- * Tests cover:
5
- * - Server routes: GET /agent/status, GET /agent/identity, GET /agent/history
6
- * - agentContext object creation and lifecycle in pave/index.js
7
- * - Iteration history ring buffer behavior
8
- * - Non-agent mode returns 404 for /agent/* routes
9
- */
10
-
11
- 'use strict';
12
-
13
- const assert = require('assert');
14
- const fs = require('fs');
15
- const path = require('path');
16
-
17
- // Read source files for static analysis
18
- const serverSource = fs.readFileSync(
19
- path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'server', 'index.js'),
20
- 'utf8',
21
- );
22
- const paveSource = fs.readFileSync(
23
- path.join(__dirname, '..', 'index.js'),
24
- 'utf8',
25
- );
26
-
27
- let passed = 0;
28
- let failed = 0;
29
- let total = 0;
30
-
31
- function runTest(name, fn) {
32
- total++;
33
- try {
34
- fn();
35
- console.log('\u2705 ' + name);
36
- passed++;
37
- } catch (e) {
38
- console.log('\u274c ' + name + ': ' + e.message);
39
- failed++;
40
- }
41
- }
42
-
43
- function assertContains(src, needle, msg) {
44
- assert(src.indexOf(needle) !== -1, (msg || '') + ' — missing: "' + needle + '"');
45
- }
46
-
47
- // =============================================================
48
- // 1. Server routes: GET /agent/status
49
- // =============================================================
50
-
51
- runTest('server has GET /agent/status route', () => {
52
- assertContains(serverSource, 'app.get("/agent/status"', 'should register /agent/status route');
53
- });
54
-
55
- runTest('server /agent/status returns 404 when not in agent mode', () => {
56
- // Route uses shared _requireAgentAccess guard which checks !agentContext
57
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
58
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
59
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
60
- // The guard itself checks agentContext and returns 404
61
- const guardIdx = serverSource.indexOf('function _requireAgentAccess(');
62
- const guardBody = serverSource.substring(guardIdx, guardIdx + 500);
63
- assertContains(guardBody, '!agentContext', 'guard should check for agentContext');
64
- assertContains(guardBody, '404', 'guard should return 404');
65
- assertContains(guardBody, 'Not running as an agent', 'guard should return descriptive error');
66
- });
67
-
68
- runTest('server /agent/status returns name, status, iteration, uptime', () => {
69
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
70
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1200);
71
- assertContains(routeBody, 'agentContext.name', 'should include agent name');
72
- assertContains(routeBody, 'agentContext.iteration', 'should include iteration count');
73
- assertContains(routeBody, 'agentContext.currentTask', 'should include currentTask');
74
- assertContains(routeBody, 'uptime', 'should calculate uptime');
75
- });
76
-
77
- runTest('server /agent/status checks runningSessions for busy state', () => {
78
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
79
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
80
- assertContains(routeBody, 'runningSessions', 'should check runningSessions for busy state');
81
- // Should check the agent's own session only, not any session
82
- assertContains(routeBody, 'agentContext.sessionId', 'should use agent session ID');
83
- assertContains(routeBody, 'runningSessions.has(agentSessionId)', 'should check agent-specific session');
84
- });
85
-
86
- runTest('server /agent/status includes consecutiveFailures and lastError', () => {
87
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
88
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1200);
89
- assertContains(routeBody, 'consecutiveFailures', 'should include consecutiveFailures');
90
- assertContains(routeBody, 'lastError', 'should include lastError');
91
- });
92
-
93
- runTest('server /agent/status includes lastActivity timestamp', () => {
94
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
95
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1200);
96
- assertContains(routeBody, 'lastActivity', 'should include lastActivity');
97
- });
98
-
99
- // =============================================================
100
- // 2. Server routes: GET /agent/identity
101
- // =============================================================
102
-
103
- runTest('server has GET /agent/identity route', () => {
104
- assertContains(serverSource, 'app.get("/agent/identity"', 'should register /agent/identity route');
105
- });
106
-
107
- runTest('server /agent/identity returns 404 when not in agent mode', () => {
108
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
109
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
110
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
111
- });
112
-
113
- runTest('server /agent/identity returns name, soulPath, pid, version', () => {
114
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
115
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
116
- assertContains(routeBody, 'agentContext.name', 'should include agent name');
117
- assertContains(routeBody, 'agentContext.soulPath', 'should include soulPath');
118
- assertContains(routeBody, 'process.pid', 'should include process PID');
119
- assertContains(routeBody, 'VERSION', 'should include server version');
120
- });
121
-
122
- runTest('server /agent/identity includes sleepMs and reinjectInterval', () => {
123
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
124
- const routeBody = serverSource.substring(routeIdx, routeIdx + 800);
125
- assertContains(routeBody, 'sleepMs', 'should include sleepMs');
126
- assertContains(routeBody, 'reinjectInterval', 'should include reinjectInterval');
127
- });
128
-
129
- runTest('server /agent/identity uses ?? null (not || null) for numeric fields', () => {
130
- // sleepMs=0 and reinjectInterval=0 are valid falsy values; || null would
131
- // incorrectly coerce them to null. Use ?? null instead.
132
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
133
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
134
- assertContains(routeBody, 'agentContext.sleepMs ?? null',
135
- 'sleepMs should use ?? null to preserve 0');
136
- assertContains(routeBody, 'agentContext.reinjectInterval ?? null',
137
- 'reinjectInterval should use ?? null to preserve 0');
138
- });
139
-
140
- runTest('server /agent/identity includes sessionId', () => {
141
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
142
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
143
- assertContains(routeBody, 'sessionId', 'should include sessionId');
144
- });
145
-
146
- // =============================================================
147
- // 3. Server routes: GET /agent/history
148
- // =============================================================
149
-
150
- runTest('server has GET /agent/history route', () => {
151
- assertContains(serverSource, 'app.get("/agent/history"', 'should register /agent/history route');
152
- });
153
-
154
- runTest('server /agent/history returns 404 when not in agent mode', () => {
155
- const routeIdx = serverSource.indexOf('app.get("/agent/history"');
156
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
157
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
158
- });
159
-
160
- runTest('server /agent/history returns history array and totalIterations', () => {
161
- const routeIdx = serverSource.indexOf('app.get("/agent/history"');
162
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
163
- assertContains(routeBody, 'history', 'should include history array');
164
- assertContains(routeBody, 'totalIterations', 'should include totalIterations');
165
- assertContains(routeBody, 'agentContext.name', 'should include agent name');
166
- });
167
-
168
- runTest('server /agent/history supports ?limit=N query param', () => {
169
- const routeIdx = serverSource.indexOf('app.get("/agent/history"');
170
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1000);
171
- assertContains(routeBody, 'req.query.limit', 'should read limit from query params');
172
- assertContains(routeBody, '.slice(', 'should slice history for limit');
173
- });
174
-
175
- // =============================================================
176
- // 4. Server: agentContext wiring
177
- // =============================================================
178
-
179
- runTest('server createServer accepts agentContext option', () => {
180
- assertContains(serverSource, 'opts.agentContext', 'should read agentContext from opts');
181
- });
182
-
183
- runTest('server agentContext defaults to null when not provided', () => {
184
- assertContains(serverSource, 'opts.agentContext || null', 'should default to null');
185
- });
186
-
187
- runTest('server defines explicit agent routes and a distinct generic /agent route', () => {
188
- // Express app.get('/agent') does not match '/agent/status' (exact match),
189
- // so route ordering is irrelevant. We only assert that all routes exist.
190
- const statusIdx = serverSource.indexOf('app.get("/agent/status"');
191
- const identityIdx = serverSource.indexOf('app.get("/agent/identity"');
192
- const historyIdx = serverSource.indexOf('app.get("/agent/history"');
193
- const genericIdx = serverSource.indexOf('app.get("/agent",');
194
- assert(statusIdx > -1, 'should have /agent/status route');
195
- assert(identityIdx > -1, 'should have /agent/identity route');
196
- assert(historyIdx > -1, 'should have /agent/history route');
197
- assert(genericIdx > -1, 'should have generic /agent route');
198
- });
199
-
200
- // =============================================================
201
- // 5. pave/index.js: agentContext object
202
- // =============================================================
203
-
204
- runTest('pave/index.js creates agentContext object in agent loop', () => {
205
- assertContains(paveSource, 'const agentContext = {', 'should create agentContext object');
206
- });
207
-
208
- runTest('pave/index.js agentContext has required fields', () => {
209
- const ctxIdx = paveSource.indexOf('const agentContext = {');
210
- const ctxBody = paveSource.substring(ctxIdx, ctxIdx + 800);
211
- assertContains(ctxBody, 'name:', 'should have name field');
212
- assertContains(ctxBody, 'soulPath:', 'should have soulPath field');
213
- assertContains(ctxBody, 'sleepMs', 'should have sleepMs field');
214
- assertContains(ctxBody, 'reinjectInterval:', 'should have reinjectInterval field');
215
- assertContains(ctxBody, 'sessionId:', 'should have sessionId field');
216
- assertContains(ctxBody, 'startedAt:', 'should have startedAt field');
217
- assertContains(ctxBody, 'iteration:', 'should have iteration field');
218
- assertContains(ctxBody, 'state:', 'should have state field');
219
- assertContains(ctxBody, 'currentTask:', 'should have currentTask field');
220
- assertContains(ctxBody, 'lastActivity:', 'should have lastActivity field');
221
- assertContains(ctxBody, 'consecutiveFailures:', 'should have consecutiveFailures field');
222
- assertContains(ctxBody, 'lastError:', 'should have lastError field');
223
- assertContains(ctxBody, 'history:', 'should have history ring buffer');
224
- });
225
-
226
- runTest('pave/index.js passes agentContext to startServer', () => {
227
- // startServer call should include agentContext
228
- assertContains(paveSource, 'agentContext })', 'should pass agentContext to startServer');
229
- });
230
-
231
- runTest('pave/index.js AGENT_HISTORY_MAX defines ring buffer size', () => {
232
- assertContains(paveSource, 'AGENT_HISTORY_MAX', 'should define AGENT_HISTORY_MAX constant');
233
- const maxIdx = paveSource.indexOf('AGENT_HISTORY_MAX = ');
234
- const maxLine = paveSource.substring(maxIdx, maxIdx + 40);
235
- assert(maxLine.indexOf('20') !== -1, 'ring buffer should have max size of 20');
236
- });
237
-
238
- // =============================================================
239
- // 6. pave/index.js: agentContext updates in agent loop
240
- // =============================================================
241
-
242
- runTest('pave/index.js updates agentContext at start of each iteration', () => {
243
- // At the top of the while(running) loop, iteration and state should be updated
244
- const loopIdx = paveSource.indexOf('// Main agent loop');
245
- const loopBody = paveSource.substring(loopIdx, loopIdx + 1000);
246
- assertContains(loopBody, 'agentContext.iteration = iteration', 'should update iteration');
247
- assertContains(loopBody, "agentContext.state = 'working'", 'should set state to working');
248
- assertContains(loopBody, 'agentContext.lastActivity', 'should update lastActivity');
249
- });
250
-
251
- runTest('pave/index.js updates agentContext when entering sleep state', () => {
252
- // Before sleeping, agentContext should reflect sleeping state
253
- const sleepIdx = paveSource.indexOf('// Update agent registry: sleeping');
254
- const sleepBody = paveSource.substring(sleepIdx, sleepIdx + 1000);
255
- assertContains(sleepBody, "agentContext.state = 'sleeping'", 'should set state to sleeping');
256
- assertContains(sleepBody, 'agentContext.currentTask', 'should update currentTask');
257
- assertContains(sleepBody, 'agentContext.consecutiveFailures', 'should update consecutiveFailures');
258
- });
259
-
260
- runTest('pave/index.js updates agentContext on circuit breaker trip', () => {
261
- const cbIdx = paveSource.indexOf('// Circuit-breaker: stop after MAX_CONSECUTIVE_FAILURES');
262
- const cbBody = paveSource.substring(cbIdx, cbIdx + 800);
263
- assertContains(cbBody, "agentContext.state = 'error'", 'should set state to error');
264
- assertContains(cbBody, 'agentContext.consecutiveFailures', 'should update failures count');
265
- assertContains(cbBody, 'agentContext.lastError', 'should update lastError');
266
- });
267
-
268
- runTest('pave/index.js updates agentContext on cleanup/stop', () => {
269
- const stopIdx = paveSource.indexOf('// Update agent registry: stopped');
270
- const stopBody = paveSource.substring(stopIdx, stopIdx + 500);
271
- assertContains(stopBody, "agentContext.state = 'stopped'", 'should set state to stopped');
272
- assertContains(stopBody, 'agentContext.iteration', 'should update final iteration');
273
- });
274
-
275
- runTest('pave/index.js updates agentContext.sessionId after compaction', () => {
276
- // When auto-compaction switches sessions, agentContext must stay in sync
277
- assertContains(paveSource, 'agentContext.sessionId = sessionId; // Keep agent context in sync',
278
- 'should update agentContext.sessionId after compaction session switch');
279
- });
280
-
281
- runTest('pave/index.js updates agentContext.sessionId after session creation', () => {
282
- // After session is resolved (created or resumed), agentContext should be updated
283
- assertContains(paveSource, 'agentContext.sessionId = sessionId;',
284
- 'should update agentContext.sessionId after session is resolved');
285
- });
286
-
287
- // =============================================================
288
- // 7. pave/index.js: iteration history ring buffer
289
- // =============================================================
290
-
291
- runTest('pave/index.js pushes iteration summaries to history', () => {
292
- // Call sites use _pushHistory helper, which internally calls agentContext.history.push
293
- assertContains(paveSource, '_pushHistory(', 'should use _pushHistory helper to push to history');
294
- });
295
-
296
- runTest('pave/index.js history entries have required fields', () => {
297
- // Check an entry passed to _pushHistory
298
- const pushIdx = paveSource.indexOf('_pushHistory({', paveSource.indexOf('Push iteration summary'));
299
- const pushBody = paveSource.substring(pushIdx, pushIdx + 300);
300
- assertContains(pushBody, 'iteration', 'history entry should have iteration');
301
- assertContains(pushBody, 'timestamp:', 'history entry should have timestamp');
302
- assertContains(pushBody, 'status:', 'history entry should have status (ok/error)');
303
- assertContains(pushBody, 'error:', 'history entry should have error field');
304
- });
305
-
306
- runTest('pave/index.js history enforces ring buffer max size via _pushHistory helper', () => {
307
- // The _pushHistory helper should enforce AGENT_HISTORY_MAX
308
- const helperIdx = paveSource.indexOf('function _pushHistory(');
309
- assert(helperIdx !== -1, 'should have _pushHistory helper');
310
- const helperBody = paveSource.substring(helperIdx, helperIdx + 300);
311
- assertContains(helperBody, 'AGENT_HISTORY_MAX', 'helper should check against max size');
312
- assertContains(helperBody, '.shift()', 'helper should shift oldest entry when full');
313
- });
314
-
315
- runTest('pave/index.js history uses shift() not splice() for ring buffer', () => {
316
- const helperIdx = paveSource.indexOf('function _pushHistory(');
317
- const helperBody = paveSource.substring(helperIdx, helperIdx + 300);
318
- assertContains(helperBody, 'agentContext.history.shift()', 'should use shift() for FIFO eviction');
319
- });
320
-
321
- // =============================================================
322
- // 8. Server route ordering and Express path matching
323
- // =============================================================
324
-
325
- runTest('server /agent/status route is separate from /agent generic route', () => {
326
- // Make sure /agent/status is a distinct route (not served by /agent)
327
- assert(serverSource.indexOf('"/agent/status"') !== -1, 'should have quoted /agent/status path');
328
- assert(serverSource.indexOf('"/agent/identity"') !== -1, 'should have quoted /agent/identity path');
329
- assert(serverSource.indexOf('"/agent/history"') !== -1, 'should have quoted /agent/history path');
330
- });
331
-
332
- runTest('server /agent/status uptime is calculated in seconds', () => {
333
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
334
- const routeBody = serverSource.substring(routeIdx, routeIdx + 1200);
335
- assertContains(routeBody, '/ 1000', 'should divide by 1000 for seconds');
336
- assertContains(routeBody, 'Math.floor', 'should floor to integer seconds');
337
- });
338
-
339
- runTest('server /agent/identity uptime is calculated in seconds', () => {
340
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
341
- const routeBody = serverSource.substring(routeIdx, routeIdx + 800);
342
- assertContains(routeBody, '/ 1000', 'should divide by 1000 for seconds');
343
- assertContains(routeBody, 'Math.floor', 'should floor to integer seconds');
344
- // Should compute now once to avoid double Date.now() causing negative uptime
345
- assertContains(routeBody, 'const now = Date.now()', 'should compute now once');
346
- assertContains(routeBody, 'now - startedAt', 'should use single now variable');
347
- });
348
-
349
- // =============================================================
350
- // 9. Comment / documentation
351
- // =============================================================
352
-
353
- runTest('server has Phase 2 issue #107 comment', () => {
354
- assertContains(serverSource, 'issue #107', 'should reference issue #107');
355
- assertContains(serverSource, 'Agent Status API', 'should describe routes as Agent Status API');
356
- });
357
-
358
- // =============================================================
359
- // 10. Loopback-only access restriction
360
- // =============================================================
361
-
362
- runTest('server has _isLoopback helper function', () => {
363
- assertContains(serverSource, 'function _isLoopback(req)', 'should define _isLoopback helper');
364
- });
365
-
366
- runTest('server _isLoopback checks full 127.0.0.0/8 range and IPv6', () => {
367
- const helperIdx = serverSource.indexOf('function _isLoopback(req)');
368
- const helperBody = serverSource.substring(helperIdx, helperIdx + 500);
369
- // Should use req.socket.remoteAddress (not deprecated req.connection)
370
- assertContains(helperBody, 'req.socket', 'should use req.socket (not deprecated req.connection)');
371
- // Should handle full 127.x.x.x range via startsWith
372
- assertContains(helperBody, 'startsWith("127.")', 'should check full 127.0.0.0/8 range');
373
- // Should handle IPv6 loopback
374
- assertContains(helperBody, '::1', 'should check IPv6 loopback');
375
- // Should handle IPv4-mapped IPv6 loopback range
376
- assertContains(helperBody, 'startsWith("::ffff:127.")', 'should check IPv4-mapped IPv6 loopback range');
377
- });
378
-
379
- runTest('server /agent/status enforces loopback-only access', () => {
380
- // Guard enforces loopback; routes use _requireAgentAccess
381
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
382
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
383
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
384
- });
385
-
386
- runTest('server /agent/identity enforces loopback-only access', () => {
387
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
388
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
389
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
390
- });
391
-
392
- runTest('server /agent/history enforces loopback-only access', () => {
393
- const routeIdx = serverSource.indexOf('app.get("/agent/history"');
394
- const routeBody = serverSource.substring(routeIdx, routeIdx + 500);
395
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
396
- });
397
-
398
- // =============================================================
399
- // 10b. Cross-origin protection for agent routes
400
- // =============================================================
401
-
402
- runTest('server has _isLocalOrigin helper function', () => {
403
- assertContains(serverSource, 'function _isLocalOrigin(req)',
404
- 'should have _isLocalOrigin helper');
405
- });
406
-
407
- runTest('server _isLocalOrigin checks Origin header hostname', () => {
408
- const helperIdx = serverSource.indexOf('function _isLocalOrigin(');
409
- const helperBody = serverSource.substring(helperIdx, helperIdx + 500);
410
- assertContains(helperBody, 'req.headers.origin', 'should read Origin header');
411
- assertContains(helperBody, 'localhost', 'should allow localhost origin');
412
- assertContains(helperBody, '127.0.0.1', 'should allow 127.0.0.1 origin');
413
- });
414
-
415
- runTest('server _isLocalOrigin allows no-Origin requests', () => {
416
- const helperIdx = serverSource.indexOf('function _isLocalOrigin(');
417
- const helperBody = serverSource.substring(helperIdx, helperIdx + 500);
418
- assertContains(helperBody, '!origin', 'should allow requests with no Origin header');
419
- assertContains(helperBody, 'return true', 'should return true for same-origin/non-browser');
420
- });
421
-
422
- runTest('server has _requireAgentAccess shared guard', () => {
423
- assertContains(serverSource, 'function _requireAgentAccess(',
424
- 'should have shared guard helper');
425
- const guardIdx = serverSource.indexOf('function _requireAgentAccess(');
426
- const guardBody = serverSource.substring(guardIdx, guardIdx + 600);
427
- assertContains(guardBody, '_isLoopback(req)', 'guard should check loopback');
428
- assertContains(guardBody, '_isLocalOrigin(req)', 'guard should check local origin');
429
- assertContains(guardBody, '!agentContext', 'guard should check agentContext');
430
- assertContains(guardBody, '403', 'guard should return 403');
431
- assertContains(guardBody, '404', 'guard should return 404');
432
- });
433
-
434
- runTest('server /agent/status uses shared guard (not inline checks)', () => {
435
- const routeIdx = serverSource.indexOf('app.get("/agent/status"');
436
- const routeBody = serverSource.substring(routeIdx, routeIdx + 300);
437
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
438
- });
439
-
440
- runTest('server /agent/identity uses shared guard (not inline checks)', () => {
441
- const routeIdx = serverSource.indexOf('app.get("/agent/identity"');
442
- const routeBody = serverSource.substring(routeIdx, routeIdx + 300);
443
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
444
- });
445
-
446
- runTest('server /agent/history uses shared guard (not inline checks)', () => {
447
- const routeIdx = serverSource.indexOf('app.get("/agent/history"');
448
- const routeBody = serverSource.substring(routeIdx, routeIdx + 300);
449
- assertContains(routeBody, '_requireAgentAccess', 'should use shared guard');
450
- });
451
-
452
- // =============================================================
453
- // 11. const vs var style consistency
454
- // =============================================================
455
-
456
- runTest('pave/index.js uses const for AGENT_HISTORY_MAX', () => {
457
- assertContains(paveSource, 'const AGENT_HISTORY_MAX = ', 'should use const for immutable constant');
458
- });
459
-
460
- runTest('pave/index.js uses const for agentContext', () => {
461
- assertContains(paveSource, 'const agentContext = {', 'should use const for object (properties are mutated, not binding)');
462
- });
463
-
464
- runTest('pave/index.js uses const for sleepTask', () => {
465
- assertContains(paveSource, 'const sleepTask = ', 'sleepTask is never reassigned, should use const');
466
- });
467
-
468
- runTest('pave/index.js has agentContext documentation comment', () => {
469
- assertContains(paveSource, 'Agent context: shared mutable',
470
- 'should document agentContext purpose');
471
- });
472
-
473
- runTest('test file uses process.exitCode not process.exit (runner-safe)', () => {
474
- const testSource = fs.readFileSync(path.join(__dirname, 'agent-status-api.test.js'), 'utf8');
475
- // Verify the file uses process.exitCode (not process.exit()) for signaling.
476
- // We can't use a simple indexOf because comments/strings in this very test
477
- // mention "process.exit(" — so we check that process.exitCode is present.
478
- assertContains(testSource, 'process.exitCode',
479
- 'should use process.exitCode for runner-safe failure signaling');
480
- });
481
-
482
- // =============================================================
483
- // 12. Circuit-breaker records final iteration in history
484
- // =============================================================
485
-
486
- runTest('circuit-breaker pushes history entry before break', () => {
487
- // The circuit-breaker block should use _pushHistory helper so /agent/history
488
- // includes the final failing iteration that caused the stop.
489
- const cbIdx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
490
- assert(cbIdx !== -1, 'should have circuit-breaker check');
491
- const breakIdx = paveSource.indexOf('trippedCircuitBreaker = true;', cbIdx);
492
- assert(breakIdx !== -1, 'should have trippedCircuitBreaker flag');
493
- const cbBlock = paveSource.substring(cbIdx, breakIdx);
494
- assertContains(cbBlock, '_pushHistory(', 'should use _pushHistory helper in circuit-breaker block');
495
- });
496
-
497
- runTest('circuit-breaker history entry has circuit-breaker status', () => {
498
- const cbIdx = paveSource.indexOf('consecutiveFailures >= MAX_CONSECUTIVE_FAILURES');
499
- const breakIdx = paveSource.indexOf('trippedCircuitBreaker = true;', cbIdx);
500
- const cbBlock = paveSource.substring(cbIdx, breakIdx);
501
- assertContains(cbBlock, "'circuit-breaker'", 'history entry should use circuit-breaker status');
502
- });
503
-
504
- runTest('_pushHistory helper exists and enforces ring buffer max', () => {
505
- const helperIdx = paveSource.indexOf('function _pushHistory(');
506
- assert(helperIdx !== -1, 'should have _pushHistory helper');
507
- const helperBody = paveSource.substring(helperIdx, helperIdx + 300);
508
- assertContains(helperBody, 'AGENT_HISTORY_MAX', 'helper should check ring buffer capacity');
509
- assertContains(helperBody, 'agentContext.history.shift()', 'helper should evict oldest entry');
510
- assertContains(helperBody, 'agentContext.history.push(entry)', 'helper should push entry');
511
- });
512
-
513
- // =============================================================
514
- // 13. SOUL read failure updates agentContext before sleeping
515
- // =============================================================
516
-
517
- runTest('SOUL read failure path sets agentContext.state to sleeping', () => {
518
- // Find the SOUL file read catch block
519
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
520
- assert(soulReadIdx !== -1, 'should have SOUL read error handler');
521
- // Extract a region around the catch block (before the continue)
522
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
523
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
524
- assertContains(catchBlock, "agentContext.state = 'sleeping'",
525
- 'should update agentContext.state to sleeping on SOUL read failure');
526
- });
527
-
528
- runTest('SOUL read failure path sets agentContext.currentTask', () => {
529
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
530
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
531
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
532
- assertContains(catchBlock, 'agentContext.currentTask',
533
- 'should update currentTask on SOUL read failure');
534
- });
535
-
536
- runTest('SOUL read failure path sets agentContext.lastError', () => {
537
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
538
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
539
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
540
- assertContains(catchBlock, 'agentContext.lastError',
541
- 'should update lastError on SOUL read failure');
542
- });
543
-
544
- runTest('SOUL read failure path increments consecutiveFailures', () => {
545
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
546
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
547
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
548
- assertContains(catchBlock, 'consecutiveFailures++',
549
- 'should increment consecutiveFailures on SOUL read failure');
550
- assertContains(catchBlock, 'agentContext.consecutiveFailures = consecutiveFailures',
551
- 'should sync agentContext.consecutiveFailures');
552
- });
553
-
554
- runTest('SOUL read failure path applies backoff sleep', () => {
555
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
556
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
557
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
558
- assertContains(catchBlock, 'MAX_BACKOFF_MS',
559
- 'should apply backoff cap on SOUL read failure');
560
- });
561
-
562
- runTest('SOUL read failure sleep wakes on inbox messages', () => {
563
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
564
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
565
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
566
- assertContains(catchBlock, 'inboxHasMessages',
567
- 'should wake on inbox messages during SOUL read backoff (like normal sleep)');
568
- });
569
-
570
- runTest('SOUL read failure path checks circuit-breaker', () => {
571
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
572
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
573
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
574
- assertContains(catchBlock, 'MAX_CONSECUTIVE_FAILURES',
575
- 'should check circuit-breaker threshold on SOUL read failure');
576
- assertContains(catchBlock, 'trippedCircuitBreaker = true',
577
- 'should trip circuit-breaker if threshold reached via SOUL errors');
578
- });
579
-
580
- runTest('SOUL read failure circuit-breaker writes ERROR status to registry', () => {
581
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
582
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
583
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
584
- assertContains(catchBlock, 'registry.writeStatus(agentName',
585
- 'should write status to on-disk registry in SOUL read failure path');
586
- });
587
-
588
- runTest('SOUL read failure non-circuit-breaker writes SLEEPING status to registry', () => {
589
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
590
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
591
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
592
- assertContains(catchBlock, 'registry.STATES.SLEEPING',
593
- 'should write SLEEPING status when not tripping circuit-breaker');
594
- });
595
-
596
- runTest('SOUL read failure path pushes history entry', () => {
597
- const soulReadIdx = paveSource.indexOf('Error reading SOUL file at');
598
- const continueIdx = paveSource.indexOf('continue;', soulReadIdx);
599
- const catchBlock = paveSource.substring(soulReadIdx, continueIdx);
600
- assertContains(catchBlock, '_pushHistory(',
601
- 'should use _pushHistory helper on SOUL read failure');
602
- });
603
-
604
- runTest('all history call-sites use _pushHistory helper (no inline eviction)', () => {
605
- // After extracting _pushHistory, there should be no inline ring-buffer
606
- // eviction logic remaining — only the helper itself should reference
607
- // agentContext.history.shift() and AGENT_HISTORY_MAX together.
608
- const helperIdx = paveSource.indexOf('function _pushHistory(');
609
- const helperEnd = paveSource.indexOf('}', helperIdx + 50);
610
- const afterHelper = paveSource.substring(helperEnd);
611
- // No direct shift+push combo should remain outside the helper
612
- const shiftCount = (afterHelper.match(/agentContext\.history\.shift\(\)/g) || []).length;
613
- assert(shiftCount === 0, 'no inline agentContext.history.shift() calls should remain outside _pushHistory (found ' + shiftCount + ')');
614
- });
615
-
616
- // =============================================================
617
- // Summary
618
- // =============================================================
619
-
620
- console.log('');
621
- console.log('Total: ' + total + ', Passed: ' + passed + ', Failed: ' + failed);
622
- if (failed > 0) {
623
- process.exitCode = 1;
624
- }