@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,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
|
-
}
|