@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,520 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Tests for agent lifecycle commands (Issue #199)
4
- *
5
- * Verifies:
6
- * - resolveAgentName() prefix matching
7
- * - getAgentLogFile() log path resolution
8
- * - stopAgent() SIGTERM + SIGKILL behavior
9
- * - args-parser recognizes stop, logs, --daemon, --follow, --lines
10
- * - handleAgentStop/handleAgentLogs routing in index.js
11
- *
12
- * Run with: node test/agent-lifecycle.test.js
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const os = require('os');
18
-
19
- let passed = 0;
20
- let failed = 0;
21
-
22
- function runTest(name, fn) {
23
- try {
24
- fn();
25
- console.log('\u2705 ' + name);
26
- passed++;
27
- } catch (e) {
28
- console.log('\u274C ' + name + ': ' + e.message);
29
- failed++;
30
- }
31
- }
32
-
33
- function assert(cond, msg) {
34
- if (!cond) throw new Error(msg || 'Assertion failed');
35
- }
36
-
37
- /**
38
- * Run an async stopAgent test in a child process so the result is
39
- * available synchronously to the test runner. The child inherits the
40
- * temp agents dir via an environment variable.
41
- */
42
- function runStopAgentTest(setupCode, assertCode) {
43
- if (!registryReady) return; // skip in sandbox
44
- const cp = require('child_process');
45
- const script = [
46
- 'var registry = require(' + JSON.stringify(require.resolve('../lib/agent-registry')) + ');',
47
- 'registry.setAgentsDir(' + JSON.stringify(tmpDir) + ');',
48
- setupCode,
49
- 'registry.stopAgent.apply(null, args).then(function(result) {',
50
- ' try {',
51
- assertCode,
52
- ' } catch (e) { process.stderr.write(e.message); process.exit(1); }',
53
- '});',
54
- ].join('\n');
55
- const r = cp.spawnSync(process.execPath, ['-e', script], {
56
- timeout: 10000,
57
- encoding: 'utf-8',
58
- });
59
- if (r.status !== 0) {
60
- throw new Error(r.stderr || 'child exited with code ' + r.status);
61
- }
62
- }
63
-
64
- // ============================================================
65
- // Args-parser tests
66
- // ============================================================
67
-
68
- const spawnSync = require('child_process').spawnSync;
69
- const parseArgs = require('../lib/args-parser').parseArgs;
70
-
71
- runTest('pave agent stop sets agentSubcommand to stop', () => {
72
- const args = parseArgs(['agent', 'stop', 'my-agent']);
73
- assert(args.command === 'agent', 'command should be agent');
74
- assert(args.agentSubcommand === 'stop', 'subcommand should be stop, got: ' + args.agentSubcommand);
75
- assert(args.commandArgs[0] === 'my-agent', 'commandArgs[0] should be my-agent');
76
- });
77
-
78
- runTest('pave agent stop without name has empty commandArgs', () => {
79
- const args = parseArgs(['agent', 'stop']);
80
- assert(args.command === 'agent', 'command should be agent');
81
- assert(args.agentSubcommand === 'stop', 'subcommand should be stop');
82
- assert(args.commandArgs.length === 0, 'commandArgs should be empty');
83
- });
84
-
85
- runTest('pave agent logs sets agentSubcommand to logs', () => {
86
- const args = parseArgs(['agent', 'logs', 'my-agent']);
87
- assert(args.command === 'agent', 'command should be agent');
88
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
89
- assert(args.commandArgs[0] === 'my-agent', 'commandArgs[0] should be my-agent');
90
- });
91
-
92
- runTest('pave agent logs without name has empty commandArgs', () => {
93
- const args = parseArgs(['agent', 'logs']);
94
- assert(args.command === 'agent', 'command should be agent');
95
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
96
- assert(args.commandArgs.length === 0, 'commandArgs should be empty');
97
- });
98
-
99
- runTest('pave agent logs --follow sets follow flag', () => {
100
- const args = parseArgs(['agent', 'logs', '--follow']);
101
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
102
- assert(args.follow === true, 'follow should be true');
103
- });
104
-
105
- runTest('pave agent logs -f sets follow flag (short flag)', () => {
106
- const args = parseArgs(['agent', 'logs', '-f']);
107
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
108
- assert(args.follow === true, 'follow should be true');
109
- });
110
-
111
- runTest('pave agent logs --lines 50 sets lines to 50', () => {
112
- const args = parseArgs(['agent', 'logs', '--lines', '50']);
113
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
114
- assert(args.lines === 50, 'lines should be 50, got: ' + args.lines);
115
- });
116
-
117
- runTest('pave agent logs -n 50 sets lines to 50 (short flag)', () => {
118
- const args = parseArgs(['agent', 'logs', '-n', '50']);
119
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
120
- assert(args.lines === 50, 'lines should be 50, got: ' + args.lines);
121
- });
122
-
123
- runTest('pave agent logs --follow --lines 20 sets both', () => {
124
- const args = parseArgs(['agent', 'logs', 'my-agent', '--follow', '--lines', '20']);
125
- assert(args.agentSubcommand === 'logs', 'subcommand should be logs');
126
- assert(args.follow === true, 'follow should be true');
127
- assert(args.lines === 20, 'lines should be 20');
128
- assert(args.commandArgs[0] === 'my-agent', 'commandArgs[0] should be my-agent');
129
- });
130
-
131
- runTest('pave agent --daemon sets daemon flag', () => {
132
- const args = parseArgs(['agent', '--daemon']);
133
- assert(args.command === 'agent', 'command should be agent');
134
- assert(args.daemon === true, 'daemon should be true');
135
- assert(!args.agentSubcommand, 'subcommand should be null');
136
- });
137
-
138
- runTest('pave agent --daemon --name test sets both', () => {
139
- const args = parseArgs(['agent', '--daemon', '--name', 'test']);
140
- assert(args.daemon === true, 'daemon should be true');
141
- assert(args.name === 'test', 'name should be test');
142
- });
143
-
144
- runTest('agent stop does not apply SOUL defaults', () => {
145
- const args = parseArgs(['agent', 'stop', 'x']);
146
- assert(args.commandArgs.length === 1, 'should have 1 arg: ' + args.commandArgs.length);
147
- // stop subcommand shouldn't have AGENTS.md default
148
- assert(args.commandArgs[0] === 'x', 'arg should be x, got: ' + args.commandArgs[0]);
149
- });
150
-
151
- runTest('agent logs does not apply SOUL defaults', () => {
152
- const args = parseArgs(['agent', 'logs']);
153
- // logs subcommand shouldn't get AGENTS.md as default
154
- assert(args.commandArgs.length === 0, 'commandArgs should be empty, got: ' + args.commandArgs.length);
155
- });
156
-
157
- runTest('agent with no subcommand still gets defaults', () => {
158
- const args = parseArgs(['agent']);
159
- assert(!args.agentSubcommand, 'subcommand should be null');
160
- assert(args.sleep === '1m', 'default sleep should be 1m');
161
- assert(args.commandArgs.length === 1, 'should have 1 default arg');
162
- assert(args.commandArgs[0] === 'AGENTS.md', 'default SOUL should be AGENTS.md');
163
- });
164
-
165
- // ============================================================
166
- // Agent registry tests (functional using temp directories)
167
- // ============================================================
168
-
169
- let registry;
170
- try {
171
- registry = require('../lib/agent-registry');
172
- } catch (e) {
173
- // May fail in sandbox; tests below will skip
174
- registry = null;
175
- }
176
-
177
- // Set up a temp directory for registry tests
178
- let tmpDir;
179
- let registryReady = false;
180
- if (registry) {
181
- try {
182
- tmpDir = path.join(os.tmpdir(), 'pave-test-' + process.pid + '-' + Date.now());
183
- fs.mkdirSync(tmpDir, { recursive: true });
184
- registry.setAgentsDir(tmpDir);
185
- registryReady = true;
186
- } catch (e) {
187
- // Sandbox may restrict mkdirSync; fall back to export-only checks
188
- registryReady = false;
189
- }
190
- }
191
-
192
- // Clean up on exit
193
- if (typeof process.on === 'function') {
194
- process.on('exit', () => {
195
- if (registryReady) {
196
- try {
197
- registry.resetAgentsDir();
198
- fs.rmSync(tmpDir, { recursive: true, force: true });
199
- } catch (_) {}
200
- }
201
- try {
202
- fs.rmSync(isolatedHome, { recursive: true, force: true });
203
- } catch (_) {}
204
- });
205
- }
206
-
207
- runTest('agent-registry exports resolveAgentName', () => {
208
- assert(registry, 'registry module should load');
209
- assert(typeof registry.resolveAgentName === 'function', 'resolveAgentName should be a function');
210
- });
211
-
212
- runTest('agent-registry exports stopAgent', () => {
213
- assert(registry, 'registry module should load');
214
- assert(typeof registry.stopAgent === 'function', 'stopAgent should be a function');
215
- });
216
-
217
- runTest('agent-registry exports getAgentLogFile', () => {
218
- assert(registry, 'registry module should load');
219
- assert(typeof registry.getAgentLogFile === 'function', 'getAgentLogFile should be a function');
220
- });
221
-
222
- runTest('resolveAgentName returns error for empty input', () => {
223
- assert(registry, 'registry module should load');
224
- const result = registry.resolveAgentName('');
225
- assert(result.name === null, 'name should be null');
226
- assert(result.error !== null, 'error should be set');
227
- });
228
-
229
- runTest('resolveAgentName rejects garbage input that sanitizes to default', () => {
230
- assert(registry, 'registry module should load');
231
- // Spaces/punctuation sanitize to empty string -> "default" via sanitizeName
232
- const result = registry.resolveAgentName(' !!! ');
233
- assert(result.name === null, 'name should be null for garbage input');
234
- assert(result.error && result.error.indexOf('Invalid') >= 0, 'error should mention invalid, got: ' + result.error);
235
- });
236
-
237
- runTest('resolveAgentName allows explicit "default" input', () => {
238
- assert(registry, 'registry module should load');
239
- // Explicit "default" should pass the guard (even if no agent named "default" exists)
240
- const result = registry.resolveAgentName('default');
241
- // May return not-found error, but should NOT return "Invalid agent name"
242
- assert(!result.error || result.error.indexOf('Invalid') === -1,
243
- 'should not say Invalid for explicit "default", got: ' + result.error);
244
- });
245
-
246
- runTest('resolveAgentName finds exact match', () => {
247
- if (!registryReady) return; // skip in sandbox
248
- // Write a status file for an agent
249
- registry.writeStatus('test-exact', { state: 'working', pid: process.pid, cwd: '/tmp' });
250
- const result = registry.resolveAgentName('test-exact');
251
- assert(result.name === 'test-exact', 'should find exact match, got: ' + result.name);
252
- assert(result.error === null, 'error should be null');
253
- });
254
-
255
- runTest('resolveAgentName finds prefix match', () => {
256
- if (!registryReady) return; // skip in sandbox
257
- registry.writeStatus('prefix-alpha-agent', { state: 'working', pid: process.pid, cwd: '/tmp' });
258
- const result = registry.resolveAgentName('prefix-alpha');
259
- assert(result.name === 'prefix-alpha-agent', 'should find prefix match, got: ' + result.name);
260
- });
261
-
262
- runTest('resolveAgentName reports ambiguous matches', () => {
263
- if (!registryReady) return; // skip in sandbox
264
- registry.writeStatus('ambig-one', { state: 'working', pid: process.pid, cwd: '/tmp' });
265
- registry.writeStatus('ambig-two', { state: 'working', pid: process.pid, cwd: '/tmp' });
266
- const result = registry.resolveAgentName('ambig');
267
- assert(result.name === null, 'name should be null for ambiguous');
268
- assert(result.error && result.error.indexOf('Ambiguous') >= 0, 'error should mention ambiguous');
269
- assert(result.candidates.length === 2, 'should have 2 candidates');
270
- });
271
-
272
- runTest('resolveAgentName returns error for no match', () => {
273
- if (!registryReady) return; // skip in sandbox
274
- const result = registry.resolveAgentName('nonexistent-xyz');
275
- assert(result.name === null, 'name should be null');
276
- assert(result.error && result.error.indexOf('No agent found') >= 0, 'error should mention not found');
277
- });
278
-
279
- runTest('getAgentLogFile returns error for unknown agent', () => {
280
- assert(registry, 'registry module should load');
281
- const result = registry.getAgentLogFile('totally-unknown-agent');
282
- assert(result.path === null, 'path should be null for unknown agent');
283
- assert(result.error !== null, 'error should be set for unknown agent');
284
- });
285
-
286
- runTest('getAgentLogFile resolves log path from status', () => {
287
- if (!registryReady) return; // skip in sandbox
288
- registry.writeStatus('log-test-agent', {
289
- state: 'working', pid: process.pid, cwd: '/home/user/project', config: '.pave',
290
- });
291
- const result = registry.getAgentLogFile('log-test-agent');
292
- assert(result.error === null, 'error should be null');
293
- assert(result.path === path.join('/home/user/project', '.pave', 'pave.log'),
294
- 'should resolve log path, got: ' + result.path);
295
- });
296
-
297
- runTest('stopAgent returns error for unknown agent', () => {
298
- assert(registry, 'registry module should load');
299
- runStopAgentTest(
300
- 'var args = ["nonexistent-stop-test"];',
301
- 'if (result.success) throw new Error("should not succeed for unknown agent");' +
302
- 'if (!result.error || result.error.indexOf("not found") < 0) throw new Error("error should mention not found, got: " + result.error);',
303
- );
304
- });
305
-
306
- runTest('stopAgent returns error for already stopped agent', () => {
307
- if (!registryReady) return; // skip in sandbox
308
- registry.writeStatus('stopped-agent', { state: 'stopped', pid: 99999 });
309
- runStopAgentTest(
310
- 'var args = ["stopped-agent"];',
311
- 'if (result.success) throw new Error("should not succeed for stopped agent");' +
312
- 'if (!result.error || result.error.indexOf("already stopped") < 0) throw new Error("error should mention already stopped, got: " + result.error);',
313
- );
314
- });
315
-
316
- runTest('stopAgent handles stale/dead process', () => {
317
- if (!registryReady) return; // skip in sandbox
318
- // PID 2147483647 should not exist on any system
319
- registry.writeStatus('stale-agent', { state: 'working', pid: 2147483647 });
320
- runStopAgentTest(
321
- 'var args = ["stale-agent"];',
322
- 'if (!result.success) throw new Error("should succeed for stale process, got: " + result.error);',
323
- );
324
- // Verify status was updated
325
- const status = registry.readStatus('stale-agent');
326
- assert(status && status.state === 'stopped', 'state should be stopped, got: ' + (status && status.state));
327
- });
328
-
329
- // ============================================================
330
- // Index.js behavioral tests (CLI spawn instead of source inspection)
331
- // ============================================================
332
- //
333
- // These tests exercise the CLI via child_process.spawnSync and assert
334
- // on exit codes and observable output, rather than reading index.js as
335
- // raw text and matching on specific substrings.
336
-
337
- const cliPath = path.join(__dirname, '..', 'index.js');
338
- const nodeBin = process.execPath || 'node';
339
-
340
- // Use an isolated HOME so the child processes won't discover the real
341
- // ~/.pave/agents/ directory (which may contain a running coder agent).
342
- let isolatedHome = path.join(os.tmpdir(), 'pave-cli-test-home-' + process.pid);
343
- let isolatedHomeReady = false;
344
- try {
345
- if (typeof fs.mkdirSync === 'function') {
346
- fs.mkdirSync(isolatedHome, { recursive: true });
347
- isolatedHomeReady = true;
348
- }
349
- } catch (_) {}
350
-
351
- // Detect whether spawnSync actually works (sandbox may stub it out).
352
- let canSpawn = false;
353
- try {
354
- const probe = spawnSync(nodeBin, ['-e', 'process.stdout.write("OK")'], {
355
- encoding: 'utf-8', timeout: 5000,
356
- });
357
- canSpawn = (probe.stdout === 'OK') && isolatedHomeReady;
358
- } catch (_) {}
359
-
360
- function runCli(cliArgs, opts) {
361
- const mergedEnv = { ...process.env,
362
- NO_COLOR: '1',
363
- HOME: isolatedHome };
364
- const mergedOpts = { encoding: 'utf-8',
365
- timeout: 5000,
366
- env: mergedEnv,
367
- cwd: isolatedHome,
368
- ...opts || {} };
369
- // Ensure env overrides from caller still use the isolated HOME
370
- if (opts && opts.env) {
371
- mergedOpts.env = { ...mergedEnv, ...opts.env };
372
- }
373
- const result = spawnSync(nodeBin, [cliPath].concat(cliArgs), mergedOpts);
374
- return {
375
- stdout: (result.stdout || '').trim(),
376
- stderr: (result.stderr || '').trim(),
377
- status: result.status,
378
- };
379
- }
380
-
381
- runTest('pave agent stop with no agent shows usage help', () => {
382
- if (!canSpawn) return; // skip when spawnSync is not reliable
383
- // Isolated HOME has no .pave/agents, so stop should give a helpful error
384
- const r = runCli(['agent', 'stop']);
385
- assert(r.status !== 0, 'exit code should be non-zero, got: ' + r.status);
386
- assert(r.stderr.includes('No agent found') || r.stderr.includes('pave agent stop'),
387
- 'should show stop usage info, got: ' + r.stderr);
388
- });
389
-
390
- runTest('pave agent logs with no log file shows error', () => {
391
- if (!canSpawn) return; // skip when spawnSync is not reliable
392
- const r = runCli(['agent', 'logs']);
393
- assert(r.status !== 0, 'exit code should be non-zero, got: ' + r.status);
394
- assert(r.stderr.includes('not found') || r.stderr.includes('Error'),
395
- 'should show log error, got: ' + r.stderr);
396
- });
397
-
398
- runTest('pave agent stop detects --name conflict with positional', () => {
399
- if (!canSpawn) return; // skip when spawnSync is not reliable
400
- const r = runCli(['agent', 'stop', 'agent-a', '--name', 'agent-b']);
401
- assert(r.status !== 0, 'exit code should be non-zero');
402
- assert(r.stderr.includes('Conflicting agent names'), 'should detect conflict, got: ' + r.stderr);
403
- });
404
-
405
- runTest('pave agent logs detects --name conflict with positional', () => {
406
- if (!canSpawn) return; // skip when spawnSync is not reliable
407
- const r = runCli(['agent', 'logs', 'agent-a', '--name', 'agent-b']);
408
- assert(r.status !== 0, 'exit code should be non-zero');
409
- assert(r.stderr.includes('Conflicting agent names'), 'should detect conflict, got: ' + r.stderr);
410
- });
411
-
412
- runTest('pave agent --help shows daemon, stop, and logs', () => {
413
- if (!canSpawn) return; // skip when spawnSync is not reliable
414
- const r = runCli(['agent', '--help']);
415
- const output = r.stdout + r.stderr;
416
- assert(output.includes('--daemon'), 'help should mention --daemon');
417
- assert(output.includes('stop'), 'help should mention stop');
418
- assert(output.includes('logs'), 'help should mention logs');
419
- });
420
-
421
- // ============================================================
422
- // Args validation tests
423
- // ============================================================
424
-
425
- runTest('stop/logs reject multiple positional args', () => {
426
- if (!canSpawn) return; // skip when spawnSync is not reliable
427
-
428
- // stop with two positional agent names should fail
429
- const rStop = runCli(['agent', 'stop', 'agent-a', 'agent-b']);
430
- assert(rStop.status !== 0, 'stop with multiple agent names should have non-zero exit code');
431
- assert(
432
- (rStop.stderr + rStop.stdout).includes('accepts at most one agent name'),
433
- 'stop should show usage error about accepting at most one agent name; got: ' + rStop.stderr,
434
- );
435
-
436
- // logs with two positional agent names should also fail
437
- const rLogs = runCli(['agent', 'logs', 'agent-a', 'agent-b']);
438
- assert(rLogs.status !== 0, 'logs with multiple agent names should have non-zero exit code');
439
- assert(
440
- (rLogs.stderr + rLogs.stdout).includes('accepts at most one agent name'),
441
- 'logs should show usage error about accepting at most one agent name; got: ' + rLogs.stderr,
442
- );
443
- });
444
-
445
- runTest('resolveAgentName returns I/O error for missing agents directory', () => {
446
- if (!registryReady) return; // skip in sandbox
447
- // Point agents dir to a non-existent path, then try to resolve
448
- const badDir = path.join(os.tmpdir(), 'pave-test-nonexistent-' + process.pid);
449
- registry.setAgentsDir(badDir);
450
- try {
451
- const result = registry.resolveAgentName('anything');
452
- // Should get an error (not found or I/O error), not a crash
453
- assert(result.error, 'should return an error when agents dir does not exist, got: ' + JSON.stringify(result));
454
- } finally {
455
- // Restore the test agents dir
456
- registry.setAgentsDir(tmpDir);
457
- }
458
- });
459
-
460
- runTest('stopAgent marks stale agent as stopped for observed PID', () => {
461
- if (!registryReady) return; // skip in sandbox
462
- // Simulate: agent had dead PID A, then a new agent overwrites status with
463
- // dead PID B (both dead in tests). stopAgent reads status, observes PID B,
464
- // and since PID B is dead it takes the stale path and writes STOPPED for
465
- // the PID it observed. The re-read guard only prevents overwriting if the
466
- // status changes between stopAgent's initial read and its write.
467
- const deadPid = 2147483647;
468
- const newPid = 2147483646;
469
- // Write initial status with dead PID
470
- registry.writeStatus('pid-race-agent', { state: 'working', pid: deadPid });
471
- // Immediately overwrite with "restarted" agent (different PID, also dead)
472
- registry.writeStatus('pid-race-agent', { state: 'working', pid: newPid });
473
- // stopAgent reads status, sees pid=newPid (dead). Takes the stale path
474
- // and marks it stopped. This is correct behavior - the guard only fires
475
- // if the PID changes AFTER the read.
476
- runStopAgentTest(
477
- 'var args = ["pid-race-agent"];',
478
- 'if (!result.success) throw new Error("should succeed, got: " + result.error);',
479
- );
480
- const finalStatus = registry.readStatus('pid-race-agent');
481
- assert(finalStatus, 'status should exist');
482
- assert(finalStatus.state === 'stopped', 'should be stopped, got: ' + finalStatus.state);
483
- });
484
-
485
- runTest('stopAgent does not call releaseAgent to avoid lock corruption', () => {
486
- // stopAgent must not directly unlink the lock file because a new agent may
487
- // have claimed it between the PID dying and the status update. claimAgent()
488
- // handles stale locks on its own.
489
- const regSource = fs.readFileSync(
490
- path.join(__dirname, '..', 'lib', 'agent-registry.js'), 'utf-8',
491
- );
492
- // Extract the stopAgent function body
493
- const fnStart = regSource.indexOf('function stopAgent(');
494
- let fnEnd = regSource.indexOf('\nfunction ', fnStart + 1);
495
- if (fnEnd === -1) fnEnd = regSource.indexOf('\nmodule.exports', fnStart + 1);
496
- const fnBody = regSource.substring(fnStart, fnEnd);
497
- // Split into lines and check for non-comment releaseAgent calls
498
- const lines = fnBody.split('\n');
499
- const activeCalls = [];
500
- for (let i = 0; i < lines.length; i++) {
501
- const trimmed = lines[i].trim();
502
- // Skip comment-only lines
503
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
504
- if (trimmed.indexOf('releaseAgent(') !== -1) {
505
- activeCalls.push(lines[i].trim());
506
- }
507
- }
508
- assert(activeCalls.length === 0,
509
- 'stopAgent should not call releaseAgent(); found: ' + activeCalls.join(' | '));
510
- });
511
-
512
- // ============================================================
513
- // Report results
514
- // ============================================================
515
-
516
- console.log('\nResults: ' + passed + ' passed, ' + failed + ' failed');
517
-
518
- if (failed > 0) {
519
- throw new Error(failed + ' agent-lifecycle test(s) failed');
520
- }