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