@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
package/test/agent-spawn.test.js
DELETED
|
@@ -1,933 +0,0 @@
|
|
|
1
|
-
// Tests for Issue #253: agent_spawn tool + pave agent restart CLI
|
|
2
|
-
// Verifies spawn tool, restart subcommand, status config persistence, and rate limiting
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const assert = require('assert');
|
|
7
|
-
|
|
8
|
-
let passed = 0;
|
|
9
|
-
let failed = 0;
|
|
10
|
-
|
|
11
|
-
function runTest(name, fn) {
|
|
12
|
-
try {
|
|
13
|
-
fn();
|
|
14
|
-
console.log('\u2705 ' + name);
|
|
15
|
-
passed++;
|
|
16
|
-
} catch (e) {
|
|
17
|
-
console.log('\u274C ' + name + ': ' + e.message);
|
|
18
|
-
failed++;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ============================================================
|
|
23
|
-
// Load source files for inspection
|
|
24
|
-
// ============================================================
|
|
25
|
-
|
|
26
|
-
const agentCommSource = fs.readFileSync(
|
|
27
|
-
path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'agent-comm.js'), 'utf8');
|
|
28
|
-
const toolsIndexSource = fs.readFileSync(
|
|
29
|
-
path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'index.js'), 'utf8');
|
|
30
|
-
const paveSource = fs.readFileSync(
|
|
31
|
-
path.join(__dirname, '..', '..', 'pave', 'index.js'), 'utf8');
|
|
32
|
-
const argsParserSource = fs.readFileSync(
|
|
33
|
-
path.join(__dirname, '..', '..', 'pave', 'lib', 'args-parser.js'), 'utf8');
|
|
34
|
-
|
|
35
|
-
// ============================================================
|
|
36
|
-
// 1. agent_spawn tool definition tests
|
|
37
|
-
// ============================================================
|
|
38
|
-
|
|
39
|
-
runTest('agent_spawn: tool definition exists in getAgentCommTools', () => {
|
|
40
|
-
assert(agentCommSource.indexOf('name: "agent_spawn"') !== -1,
|
|
41
|
-
'should have agent_spawn tool definition');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
runTest('agent_spawn: tool has required parameters (name and soul)', () => {
|
|
45
|
-
const toolIdx = agentCommSource.indexOf('name: "agent_spawn"');
|
|
46
|
-
const toolSection = agentCommSource.substring(toolIdx, toolIdx + 2000);
|
|
47
|
-
assert(toolSection.indexOf('"name"') !== -1, 'should have name parameter');
|
|
48
|
-
assert(toolSection.indexOf('"soul"') !== -1, 'should have soul parameter');
|
|
49
|
-
assert(toolSection.indexOf('required: ["name", "soul"]') !== -1,
|
|
50
|
-
'name and soul should be required');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
runTest('agent_spawn: tool has optional parameters', () => {
|
|
54
|
-
const toolIdx = agentCommSource.indexOf('name: "agent_spawn"');
|
|
55
|
-
const toolSection = agentCommSource.substring(toolIdx, toolIdx + 2000);
|
|
56
|
-
assert(toolSection.indexOf('working_directory') !== -1, 'should have working_directory param');
|
|
57
|
-
assert(toolSection.indexOf('sleep') !== -1, 'should have sleep param');
|
|
58
|
-
assert(toolSection.indexOf('reinject_interval') !== -1, 'should have reinject_interval param');
|
|
59
|
-
assert(toolSection.indexOf('config') !== -1, 'should have config param');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// ============================================================
|
|
63
|
-
// 2. agent_spawn executor tests
|
|
64
|
-
// ============================================================
|
|
65
|
-
|
|
66
|
-
runTest('agent_spawn: executeAgentSpawn function exists', () => {
|
|
67
|
-
assert(agentCommSource.indexOf('function executeAgentSpawn(') !== -1,
|
|
68
|
-
'should define executeAgentSpawn function');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
runTest('agent_spawn: validates required name parameter', () => {
|
|
72
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
73
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 500);
|
|
74
|
-
assert(execSection.indexOf("!args.name") !== -1,
|
|
75
|
-
'should check for missing name parameter');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
runTest('agent_spawn: validates required soul parameter', () => {
|
|
79
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
80
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 500);
|
|
81
|
-
assert(execSection.indexOf("!args.soul") !== -1,
|
|
82
|
-
'should check for missing soul parameter');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
runTest('agent_spawn: checks if agent is already alive (handles object return)', () => {
|
|
86
|
-
assert(agentCommSource.indexOf('isAgentAlive(agentName)') !== -1,
|
|
87
|
-
'should check if agent name is already running');
|
|
88
|
-
// isAgentAlive returns { alive, status } - must extract .alive boolean
|
|
89
|
-
const aliveIdx = agentCommSource.indexOf('_registry.isAgentAlive(agentName)');
|
|
90
|
-
assert(aliveIdx !== -1, 'should find _registry.isAgentAlive(agentName) call');
|
|
91
|
-
const aliveSection = agentCommSource.substring(aliveIdx, aliveIdx + 300);
|
|
92
|
-
assert(aliveSection.indexOf('.alive') !== -1,
|
|
93
|
-
'should extract .alive from isAgentAlive result (not treat object as boolean)');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
runTest('agent_spawn: validates SOUL file exists', () => {
|
|
97
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
98
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 3000);
|
|
99
|
-
assert(execSection.indexOf('statSync(resolvedSoul)') !== -1,
|
|
100
|
-
'should validate SOUL file via statSync');
|
|
101
|
-
assert(execSection.indexOf('SOUL file not found') !== -1 || execSection.indexOf('isFile()') !== -1,
|
|
102
|
-
'should validate SOUL is a file or show missing error');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
runTest('agent_spawn: spawns with detached mode and PAVE_DAEMON env', () => {
|
|
106
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
107
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 7000);
|
|
108
|
-
assert(execSection.indexOf('detached: true') !== -1,
|
|
109
|
-
'should spawn with detached: true');
|
|
110
|
-
assert(execSection.indexOf("PAVE_DAEMON: '1'") !== -1,
|
|
111
|
-
'should set PAVE_DAEMON=1 env var');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
runTest('agent_spawn: calls child.unref() to detach from parent', () => {
|
|
115
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
116
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 7000);
|
|
117
|
-
assert(execSection.indexOf('child.unref()') !== -1,
|
|
118
|
-
'should call child.unref() for daemon detachment');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
runTest('agent_spawn: returns success with PID and name', () => {
|
|
122
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
123
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 7000);
|
|
124
|
-
assert(execSection.indexOf('pid: child.pid') !== -1,
|
|
125
|
-
'should return PID in result');
|
|
126
|
-
assert(execSection.indexOf('name: agentName') !== -1,
|
|
127
|
-
'should return agent name in result');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ============================================================
|
|
131
|
-
// 3. Spawn rate limiting tests
|
|
132
|
-
// ============================================================
|
|
133
|
-
|
|
134
|
-
runTest('agent_spawn: spawn rate limit constant defined (3/min)', () => {
|
|
135
|
-
assert(agentCommSource.indexOf('SPAWN_RATE_LIMIT_MAX = 3') !== -1,
|
|
136
|
-
'should define SPAWN_RATE_LIMIT_MAX = 3');
|
|
137
|
-
assert(agentCommSource.indexOf('SPAWN_RATE_LIMIT_WINDOW_MS = 60 * 1000') !== -1,
|
|
138
|
-
'should define 60-second spawn rate limit window');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
runTest('agent_spawn: max total agents guard (10)', () => {
|
|
142
|
-
assert(agentCommSource.indexOf('MAX_TOTAL_AGENTS = 10') !== -1,
|
|
143
|
-
'should define MAX_TOTAL_AGENTS = 10');
|
|
144
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
145
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 3000);
|
|
146
|
-
assert(execSection.indexOf('aliveCount >= MAX_TOTAL_AGENTS') !== -1,
|
|
147
|
-
'should check alive count against max total');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
runTest('agent_spawn: warns when many agents running (>= 3)', () => {
|
|
151
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
152
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 3000);
|
|
153
|
-
assert(execSection.indexOf('aliveCount >= 3') !== -1,
|
|
154
|
-
'should warn when >= 3 agents running');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
runTest('agent_spawn: _checkSpawnRateLimit function exists', () => {
|
|
158
|
-
assert(agentCommSource.indexOf('function _checkSpawnRateLimit()') !== -1,
|
|
159
|
-
'should define _checkSpawnRateLimit function');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
runTest('agent_spawn: spawn rate limit resets window correctly', () => {
|
|
163
|
-
const agentComm = require(path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'agent-comm.js'));
|
|
164
|
-
// Reset state
|
|
165
|
-
agentComm._spawnRateState.count = 0;
|
|
166
|
-
agentComm._spawnRateState.windowStart = 0;
|
|
167
|
-
// First check should be allowed
|
|
168
|
-
let result = agentComm._checkSpawnRateLimit();
|
|
169
|
-
assert(result.allowed === true, 'first check should be allowed');
|
|
170
|
-
// Second and third should also be allowed
|
|
171
|
-
result = agentComm._checkSpawnRateLimit();
|
|
172
|
-
assert(result.allowed === true, 'second check should be allowed');
|
|
173
|
-
result = agentComm._checkSpawnRateLimit();
|
|
174
|
-
assert(result.allowed === true, 'third check should be allowed');
|
|
175
|
-
// Fourth should be rate limited
|
|
176
|
-
result = agentComm._checkSpawnRateLimit();
|
|
177
|
-
assert(result.allowed === false, 'fourth check should be rate limited');
|
|
178
|
-
assert(result.error.indexOf('Spawn rate limited') !== -1, 'should have rate limit error message');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
runTest('agent_spawn: spawn rate limit window expires', () => {
|
|
182
|
-
const agentComm = require(path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'agent-comm.js'));
|
|
183
|
-
// Set window start to past (> 60s ago) with count at max
|
|
184
|
-
const state = agentComm._spawnRateState;
|
|
185
|
-
state.count = agentComm.SPAWN_RATE_LIMIT_MAX;
|
|
186
|
-
state.windowStart = Date.now() - (agentComm.SPAWN_RATE_LIMIT_WINDOW_MS + 1000);
|
|
187
|
-
// Should reset window and allow
|
|
188
|
-
const result = agentComm._checkSpawnRateLimit();
|
|
189
|
-
assert(result.allowed === true, 'should be allowed after window expires');
|
|
190
|
-
// After reset, count should be 1 (the new request)
|
|
191
|
-
assert(state.count === 1, 'count should reset to 1 after window expires, got ' + state.count);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// ============================================================
|
|
195
|
-
// 4. Module exports tests
|
|
196
|
-
// ============================================================
|
|
197
|
-
|
|
198
|
-
runTest('agent_spawn: executeAgentSpawn is exported', () => {
|
|
199
|
-
// Check for shorthand or explicit property
|
|
200
|
-
assert(agentCommSource.indexOf('executeAgentSpawn') !== -1
|
|
201
|
-
&& /module\.exports\s*=\s*\{[^}]*executeAgentSpawn/s.test(agentCommSource),
|
|
202
|
-
'should export executeAgentSpawn');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
runTest('agent_spawn: SPAWN_RATE_LIMIT_MAX is exported', () => {
|
|
206
|
-
assert(/module\.exports\s*=\s*\{[^}]*SPAWN_RATE_LIMIT_MAX/s.test(agentCommSource),
|
|
207
|
-
'should export SPAWN_RATE_LIMIT_MAX');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
runTest('agent_spawn: MAX_TOTAL_AGENTS is exported', () => {
|
|
211
|
-
assert(/module\.exports\s*=\s*\{[^}]*MAX_TOTAL_AGENTS/s.test(agentCommSource),
|
|
212
|
-
'should export MAX_TOTAL_AGENTS');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// ============================================================
|
|
216
|
-
// 5. tools/index.js wiring tests
|
|
217
|
-
// ============================================================
|
|
218
|
-
|
|
219
|
-
runTest('tools/index.js: imports executeAgentSpawn', () => {
|
|
220
|
-
assert(toolsIndexSource.indexOf('executeAgentSpawn') !== -1,
|
|
221
|
-
'should import executeAgentSpawn from agent-comm');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
runTest('tools/index.js: dispatches agent_spawn tool calls', () => {
|
|
225
|
-
assert(toolsIndexSource.indexOf('name === "agent_spawn"') !== -1,
|
|
226
|
-
'should have agent_spawn dispatch check');
|
|
227
|
-
const dispatchIdx = toolsIndexSource.indexOf('name === "agent_spawn"');
|
|
228
|
-
const dispatchSection = toolsIndexSource.substring(dispatchIdx, dispatchIdx + 300);
|
|
229
|
-
assert(dispatchSection.indexOf('executeAgentSpawn(args, toolId)') !== -1,
|
|
230
|
-
'should call executeAgentSpawn with args and toolId');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ============================================================
|
|
234
|
-
// 6. args-parser: restart subcommand tests
|
|
235
|
-
// ============================================================
|
|
236
|
-
|
|
237
|
-
runTest('args-parser: recognizes restart as agentSubcommand', () => {
|
|
238
|
-
assert(argsParserSource.indexOf("'restart'") !== -1,
|
|
239
|
-
'should recognize restart as a subcommand');
|
|
240
|
-
// Check it's in the subcommand assignment block
|
|
241
|
-
const restartIdx = argsParserSource.indexOf("'restart'");
|
|
242
|
-
const nearbySection = argsParserSource.substring(restartIdx - 200, restartIdx + 200);
|
|
243
|
-
assert(nearbySection.indexOf('agentSubcommand') !== -1,
|
|
244
|
-
'restart should be in agentSubcommand assignment context');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
runTest('args-parser: restart subcommand accepts agent name positional', () => {
|
|
248
|
-
// The validation guard that accepts agent name should include restart
|
|
249
|
-
assert(argsParserSource.indexOf("'stop' || a === 'logs' || a === 'restart'") !== -1,
|
|
250
|
-
'restart should be alongside stop/logs for name positional acceptance');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
runTest('args-parser: restart appears in help text', () => {
|
|
254
|
-
assert(argsParserSource.indexOf('pave agent restart') !== -1,
|
|
255
|
-
'restart should appear in usage/help text');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
runTest('args-parser: functional - parse "pave agent restart designer"', () => {
|
|
259
|
-
const parseArgsMod = require(path.join(__dirname, '..', '..', 'pave', 'lib', 'args-parser.js'));
|
|
260
|
-
const parseFn = parseArgsMod.parseArgs || parseArgsMod;
|
|
261
|
-
assert(typeof parseFn === 'function', 'should be able to get parseArgs function, got ' + typeof parseFn);
|
|
262
|
-
const args = parseFn(['agent', 'restart', 'designer']);
|
|
263
|
-
assert(args.command === 'agent', 'should set command to agent');
|
|
264
|
-
assert(args.agentSubcommand === 'restart', 'should set agentSubcommand to restart');
|
|
265
|
-
assert(args.commandArgs[0] === 'designer', 'should capture agent name as commandArgs[0]');
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
runTest('args-parser: functional - parse "pave agent restart" (no name)', () => {
|
|
269
|
-
const parseArgsMod = require(path.join(__dirname, '..', '..', 'pave', 'lib', 'args-parser.js'));
|
|
270
|
-
const parseFn = parseArgsMod.parseArgs || parseArgsMod;
|
|
271
|
-
const args = parseFn(['agent', 'restart']);
|
|
272
|
-
assert(args.command === 'agent', 'should set command to agent');
|
|
273
|
-
assert(args.agentSubcommand === 'restart', 'should set agentSubcommand to restart');
|
|
274
|
-
assert(!args.commandArgs || args.commandArgs.length === 0, 'should have no commandArgs');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
runTest('args-parser: functional - parse "pave agent start hr-manager"', () => {
|
|
278
|
-
const parseArgsMod = require(path.join(__dirname, '..', '..', 'pave', 'lib', 'args-parser.js'));
|
|
279
|
-
const parseFn = parseArgsMod.parseArgs || parseArgsMod;
|
|
280
|
-
assert(typeof parseFn === 'function', 'should be able to get parseArgs function');
|
|
281
|
-
const args = parseFn(['agent', 'start', 'hr-manager']);
|
|
282
|
-
assert(args.command === 'agent', 'should set command to agent');
|
|
283
|
-
assert(args.agentSubcommand === 'start', 'should set agentSubcommand to start');
|
|
284
|
-
assert(args.commandArgs[0] === 'hr-manager', 'should capture agent name as commandArgs[0]');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
runTest('args-parser: functional - parse "pave agent start" (no name)', () => {
|
|
288
|
-
const parseArgsMod = require(path.join(__dirname, '..', '..', 'pave', 'lib', 'args-parser.js'));
|
|
289
|
-
const parseFn = parseArgsMod.parseArgs || parseArgsMod;
|
|
290
|
-
const args = parseFn(['agent', 'start']);
|
|
291
|
-
assert(args.command === 'agent', 'should set command to agent');
|
|
292
|
-
assert(args.agentSubcommand === 'start', 'should set agentSubcommand to start');
|
|
293
|
-
assert(!args.commandArgs || args.commandArgs.length === 0, 'should have no commandArgs');
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ============================================================
|
|
297
|
-
// 7. pave/index.js: restart handler tests
|
|
298
|
-
// ============================================================
|
|
299
|
-
|
|
300
|
-
runTest('index.js: handleAgentRestart function exists', () => {
|
|
301
|
-
assert(paveSource.indexOf('function handleAgentRestart(') !== -1,
|
|
302
|
-
'should define handleAgentRestart function');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
runTest('index.js: restart subcommand is dispatched', () => {
|
|
306
|
-
assert(paveSource.indexOf("agentSubcommand === 'restart'") !== -1,
|
|
307
|
-
'should check for restart subcommand');
|
|
308
|
-
const restartIdx = paveSource.indexOf("agentSubcommand === 'restart'");
|
|
309
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 200);
|
|
310
|
-
assert(restartSection.indexOf('handleAgentRestart') !== -1,
|
|
311
|
-
'should call handleAgentRestart');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
runTest('index.js: restart reads saved config from status', () => {
|
|
315
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
316
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 4000);
|
|
317
|
-
assert(restartSection.indexOf('status.soulPath') !== -1,
|
|
318
|
-
'should read soulPath from status');
|
|
319
|
-
assert(restartSection.indexOf('status.sleepMs') !== -1,
|
|
320
|
-
'should read sleepMs from status');
|
|
321
|
-
assert(restartSection.indexOf('status.reinjectInterval') !== -1,
|
|
322
|
-
'should read reinjectInterval from status');
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
runTest('index.js: restart validates soulPath exists in status', () => {
|
|
326
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
327
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 3000);
|
|
328
|
-
assert(restartSection.indexOf('!status.soulPath') !== -1,
|
|
329
|
-
'should check if soulPath is missing in status');
|
|
330
|
-
assert(restartSection.indexOf('has no soulPath') !== -1,
|
|
331
|
-
'should have error message for missing soulPath');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
runTest('index.js: restart preserves session ID', () => {
|
|
335
|
-
// handleAgentRestart reads sessionId from status and passes it to _spawnFromStatus
|
|
336
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
337
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 7000);
|
|
338
|
-
assert(restartSection.indexOf('status.sessionId') !== -1,
|
|
339
|
-
'should read sessionId from status');
|
|
340
|
-
assert(restartSection.indexOf('_spawnFromStatus') !== -1,
|
|
341
|
-
'should delegate to _spawnFromStatus');
|
|
342
|
-
// _spawnFromStatus passes --session flag
|
|
343
|
-
const spawnHelper = paveSource.substring(paveSource.indexOf('function _spawnFromStatus('),
|
|
344
|
-
paveSource.indexOf('function _spawnFromStatus(') + 3000);
|
|
345
|
-
assert(spawnHelper.indexOf("'--session'") !== -1,
|
|
346
|
-
'should pass --session flag when re-spawning');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
runTest('index.js: restart stops agent before re-spawning', () => {
|
|
350
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
351
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 7000);
|
|
352
|
-
assert(restartSection.indexOf('registry.stopAgent(agentName)') !== -1,
|
|
353
|
-
'should call stopAgent before respawning');
|
|
354
|
-
// Stop should come before _spawnFromStatus call
|
|
355
|
-
const stopIdx = restartSection.indexOf('stopAgent');
|
|
356
|
-
const spawnIdx = restartSection.indexOf('_spawnFromStatus');
|
|
357
|
-
assert(stopIdx < spawnIdx, 'stop should come before spawn');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
runTest('index.js: restart spawns detached daemon', () => {
|
|
361
|
-
// Spawn details are now in the shared _spawnFromStatus helper
|
|
362
|
-
const spawnHelperIdx = paveSource.indexOf('function _spawnFromStatus(');
|
|
363
|
-
const spawnSection = paveSource.substring(spawnHelperIdx, spawnHelperIdx + 3000);
|
|
364
|
-
assert(spawnSection.indexOf('detached: true') !== -1,
|
|
365
|
-
'should spawn with detached: true');
|
|
366
|
-
assert(spawnSection.indexOf("PAVE_DAEMON: '1'") !== -1,
|
|
367
|
-
'should set PAVE_DAEMON env var');
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
runTest('index.js: restart has no unused spawn import (delegated to _spawnFromStatus)', () => {
|
|
371
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
372
|
-
// Find the end of handleAgentRestart by looking for the next top-level function
|
|
373
|
-
const nextFnIdx = paveSource.indexOf('\nfunction ', restartIdx + 1);
|
|
374
|
-
const nextAsyncFnIdx = paveSource.indexOf('\nasync function ', restartIdx + 1);
|
|
375
|
-
// Use the nearest one
|
|
376
|
-
const candidates = [];
|
|
377
|
-
if (nextFnIdx !== -1) candidates.push(nextFnIdx);
|
|
378
|
-
if (nextAsyncFnIdx !== -1) candidates.push(nextAsyncFnIdx);
|
|
379
|
-
const endIdx = candidates.length > 0 ? Math.min.apply(null, candidates) : paveSource.length;
|
|
380
|
-
const restartBody = paveSource.substring(restartIdx, endIdx);
|
|
381
|
-
assert(restartBody.indexOf("require('child_process')") === -1,
|
|
382
|
-
'handleAgentRestart should not import child_process (spawning is in _spawnFromStatus)');
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
runTest('index.js: restart infers agent from CWD when no name given', () => {
|
|
386
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
387
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 2000);
|
|
388
|
-
// Should have same CWD inference as handleAgentStop
|
|
389
|
-
assert(restartSection.indexOf('process.cwd()') !== -1,
|
|
390
|
-
'should use process.cwd() for CWD-based inference');
|
|
391
|
-
assert(restartSection.indexOf("a.status.cwd === cwd") !== -1,
|
|
392
|
-
'should match agent status CWD');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
runTest('index.js: restart uses resolveAgentName for prefix matching', () => {
|
|
396
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
397
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 3000);
|
|
398
|
-
assert(restartSection.indexOf('registry.resolveAgentName(nameArg)') !== -1,
|
|
399
|
-
'should use resolveAgentName for prefix matching');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// ============================================================
|
|
403
|
-
// 7b. Copilot review fixes (Issue #258)
|
|
404
|
-
// ============================================================
|
|
405
|
-
|
|
406
|
-
runTest('index.js: restart validates SOUL file using validateSoulFile()', () => {
|
|
407
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
408
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 4000);
|
|
409
|
-
assert(restartSection.indexOf('validateSoulFile(status.soulPath)') !== -1,
|
|
410
|
-
'should call validateSoulFile() for SOUL validation');
|
|
411
|
-
assert(restartSection.indexOf('soulValidation.valid') !== -1 || restartSection.indexOf('!soulValidation.valid') !== -1,
|
|
412
|
-
'should check validation result');
|
|
413
|
-
assert(restartSection.indexOf('Cannot restart') !== -1,
|
|
414
|
-
'should have clear error when SOUL validation fails');
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
runTest('index.js: restart uses explicit index.js for pavePath', () => {
|
|
418
|
-
// pavePath is now in the shared _spawnFromStatus helper
|
|
419
|
-
const spawnHelperIdx = paveSource.indexOf('function _spawnFromStatus(');
|
|
420
|
-
const spawnSection = paveSource.substring(spawnHelperIdx, spawnHelperIdx + 3000);
|
|
421
|
-
assert(spawnSection.indexOf("path.join(__dirname, 'index.js')") !== -1,
|
|
422
|
-
'should use path.join(__dirname, index.js) not path.resolve(__dirname)');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
runTest('index.js: restart destructures isAgentAlive return value', () => {
|
|
426
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
427
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 5000);
|
|
428
|
-
assert(restartSection.indexOf('aliveResult') !== -1,
|
|
429
|
-
'should store isAgentAlive result as aliveResult');
|
|
430
|
-
assert(restartSection.indexOf('aliveResult.alive') !== -1,
|
|
431
|
-
'should check .alive property from isAgentAlive result');
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// ============================================================
|
|
435
|
-
// 8. handleAgentStart tests (Issue #266)
|
|
436
|
-
// ============================================================
|
|
437
|
-
|
|
438
|
-
runTest('index.js: handleAgentStart function exists', () => {
|
|
439
|
-
assert(paveSource.indexOf('function handleAgentStart(') !== -1,
|
|
440
|
-
'should have handleAgentStart function');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
runTest('index.js: start subcommand is dispatched', () => {
|
|
444
|
-
assert(paveSource.indexOf("agentSubcommand === 'start'") !== -1,
|
|
445
|
-
'should check for start subcommand');
|
|
446
|
-
assert(paveSource.indexOf('handleAgentStart(args)') !== -1,
|
|
447
|
-
'should call handleAgentStart');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
runTest('args-parser: recognizes start as agentSubcommand', () => {
|
|
451
|
-
assert(argsParserSource.indexOf("'start'") !== -1,
|
|
452
|
-
'should include start in subcommand detection');
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
runTest('args-parser: start appears in help text', () => {
|
|
456
|
-
assert(argsParserSource.indexOf('pave agent start') !== -1,
|
|
457
|
-
'help text should mention pave agent start');
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
runTest('index.js: start refuses if agent is already running', () => {
|
|
461
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
462
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
463
|
-
assert(startSection.indexOf('isAgentAlive') !== -1,
|
|
464
|
-
'should check if agent is alive');
|
|
465
|
-
assert(startSection.indexOf('already running') !== -1,
|
|
466
|
-
'should print error when agent is already running');
|
|
467
|
-
assert(startSection.indexOf('pave agent restart') !== -1,
|
|
468
|
-
'should suggest using restart instead');
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
runTest('index.js: start reads saved config from status', () => {
|
|
472
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
473
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
474
|
-
assert(startSection.indexOf('registry.readStatus(agentName)') !== -1,
|
|
475
|
-
'should read status from registry');
|
|
476
|
-
assert(startSection.indexOf('status.soulPath') !== -1,
|
|
477
|
-
'should access soulPath from status');
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
runTest('index.js: start validates soulPath exists in status', () => {
|
|
481
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
482
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
483
|
-
assert(startSection.indexOf('!status.soulPath') !== -1,
|
|
484
|
-
'should check soulPath is present in status');
|
|
485
|
-
assert(startSection.indexOf('Cannot start') !== -1,
|
|
486
|
-
'should have clear error when no soulPath');
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
runTest('index.js: start validates SOUL file exists', () => {
|
|
490
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
491
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
492
|
-
assert(startSection.indexOf('validateSoulFile(status.soulPath)') !== -1,
|
|
493
|
-
'should call validateSoulFile');
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
runTest('index.js: start validates cwd still exists and is a directory', () => {
|
|
497
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
498
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
499
|
-
assert(startSection.indexOf('fs.statSync(agentCwd)') !== -1 ||
|
|
500
|
-
startSection.indexOf('cwdStat') !== -1,
|
|
501
|
-
'should validate working directory still exists');
|
|
502
|
-
assert(startSection.indexOf('isDirectory') !== -1,
|
|
503
|
-
'should verify cwd is actually a directory');
|
|
504
|
-
assert(startSection.indexOf('no longer exists') !== -1,
|
|
505
|
-
'should print error when cwd is missing');
|
|
506
|
-
// Should not fall back to process.cwd() — require explicit cwd in status
|
|
507
|
-
assert(startSection.indexOf("status.cwd || process.cwd()") === -1,
|
|
508
|
-
'should not fall back to process.cwd() for agent cwd');
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
runTest('index.js: start delegates to _spawnFromStatus', () => {
|
|
512
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
513
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
514
|
-
assert(startSection.indexOf('_spawnFromStatus') !== -1,
|
|
515
|
-
'should delegate to shared spawn helper');
|
|
516
|
-
assert(startSection.indexOf("'started'") !== -1,
|
|
517
|
-
'should use started as verb');
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
runTest('index.js: start infers agent from CWD (includes stopped agents)', () => {
|
|
521
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
522
|
-
const startSection = paveSource.substring(startIdx, startIdx + 3000);
|
|
523
|
-
assert(startSection.indexOf('process.cwd()') !== -1,
|
|
524
|
-
'should use process.cwd() for inference');
|
|
525
|
-
// start should find stopped agents too (not just alive ones)
|
|
526
|
-
assert(startSection.indexOf('cwdMatches') !== -1,
|
|
527
|
-
'should match all agents in cwd (not just alive)');
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
runTest('index.js: start uses resolveAgentName for prefix matching', () => {
|
|
531
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
532
|
-
const startSection = paveSource.substring(startIdx, startIdx + 3000);
|
|
533
|
-
assert(startSection.indexOf('registry.resolveAgentName(nameArg)') !== -1,
|
|
534
|
-
'should use resolveAgentName for prefix matching');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
runTest('index.js: start preserves session from saved config', () => {
|
|
538
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
539
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
540
|
-
assert(startSection.indexOf('status.sessionId') !== -1,
|
|
541
|
-
'should read sessionId from status');
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
runTest('index.js: _spawnFromStatus shared helper exists', () => {
|
|
545
|
-
assert(paveSource.indexOf('function _spawnFromStatus(') !== -1,
|
|
546
|
-
'should have _spawnFromStatus helper function');
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
runTest('index.js: _spawnFromStatus handles both start and restart verbs', () => {
|
|
550
|
-
const helperIdx = paveSource.indexOf('function _spawnFromStatus(');
|
|
551
|
-
const helperSection = paveSource.substring(helperIdx, helperIdx + 300);
|
|
552
|
-
assert(helperSection.indexOf('verb') !== -1,
|
|
553
|
-
'should accept verb parameter for user messages');
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
runTest('index.js: _spawnFromStatus uses ACTION_LABELS instead of brittle verb.replace', () => {
|
|
557
|
-
const helperIdx = paveSource.indexOf('function _spawnFromStatus(');
|
|
558
|
-
const helperSection = paveSource.substring(helperIdx, helperIdx + 3000);
|
|
559
|
-
assert(helperSection.indexOf('ACTION_LABELS') !== -1,
|
|
560
|
-
'should use ACTION_LABELS map for verb-to-action conversion');
|
|
561
|
-
assert(helperSection.indexOf("verb.replace('ed'") === -1,
|
|
562
|
-
'should not use brittle verb.replace() pattern');
|
|
563
|
-
assert(helperSection.indexOf("action + ' agent") !== -1,
|
|
564
|
-
'should use action variable in error messages');
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
runTest('index.js: _spawnFromStatus passes --port when saved in config', () => {
|
|
568
|
-
const helperIdx = paveSource.indexOf('function _spawnFromStatus(');
|
|
569
|
-
const helperSection = paveSource.substring(helperIdx, helperIdx + 3000);
|
|
570
|
-
assert(helperSection.indexOf("savedConfig.port") !== -1,
|
|
571
|
-
'should check for port in savedConfig');
|
|
572
|
-
assert(helperSection.indexOf("'--port'") !== -1,
|
|
573
|
-
'should pass --port flag to child args');
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
runTest('index.js: start savedConfig includes port from status', () => {
|
|
577
|
-
const startIdx = paveSource.indexOf('function handleAgentStart(');
|
|
578
|
-
const startSection = paveSource.substring(startIdx, startIdx + 5000);
|
|
579
|
-
assert(startSection.indexOf('port: status.port') !== -1,
|
|
580
|
-
'should carry port from status.json into savedConfig');
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
runTest('index.js: restart savedConfig includes port from status', () => {
|
|
584
|
-
const restartIdx = paveSource.indexOf('function handleAgentRestart(');
|
|
585
|
-
const restartSection = paveSource.substring(restartIdx, restartIdx + 5000);
|
|
586
|
-
assert(restartSection.indexOf('port: status.port') !== -1,
|
|
587
|
-
'should carry port from status.json into savedConfig');
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
runTest('index.js: help text includes start subcommand', () => {
|
|
591
|
-
assert(paveSource.indexOf('pave agent start') !== -1,
|
|
592
|
-
'agent help should mention start');
|
|
593
|
-
assert(paveSource.indexOf('Start a stopped agent') !== -1,
|
|
594
|
-
'help should describe start functionality');
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
runTest('index.js: examples include start command', () => {
|
|
598
|
-
assert(paveSource.indexOf('pave agent start github-watcher') !== -1,
|
|
599
|
-
'examples should show start command');
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// ============================================================
|
|
603
|
-
// 8b. Port field fix (Issue #266)
|
|
604
|
-
// ============================================================
|
|
605
|
-
|
|
606
|
-
runTest('index.js: writeStatus uses actualPort instead of PORT constant', () => {
|
|
607
|
-
// After the server starts, status should use the actual listening port
|
|
608
|
-
// The declaration of actualPort should exist in the agent daemon handler
|
|
609
|
-
const daemonIdx = paveSource.indexOf('let actualPort = PORT');
|
|
610
|
-
assert(daemonIdx !== -1, 'should declare actualPort variable initialized to PORT');
|
|
611
|
-
|
|
612
|
-
// After startServer, actualPort should be updated
|
|
613
|
-
assert(paveSource.indexOf('actualPort = serverResult.actualPort') !== -1,
|
|
614
|
-
'should update actualPort from serverResult after startServer');
|
|
615
|
-
|
|
616
|
-
// writeStatus calls should use actualPort, not PORT
|
|
617
|
-
// Find the first writeStatus after startServer in the agent handler
|
|
618
|
-
const serverStartIdx = paveSource.indexOf('serverResult = await startServer');
|
|
619
|
-
const afterServer = paveSource.substring(serverStartIdx, serverStartIdx + 20000);
|
|
620
|
-
const writeStatusInSection = afterServer.indexOf('port: actualPort');
|
|
621
|
-
assert(writeStatusInSection !== -1,
|
|
622
|
-
'writeStatus after startServer should use actualPort');
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
runTest('index.js: SOUL-read failure circuit-breaker uses actualPort', () => {
|
|
626
|
-
// When the circuit breaker trips after max SOUL read failures,
|
|
627
|
-
// the registry write should use actualPort, not PORT
|
|
628
|
-
// Use the unique string that only appears in writeStatus (not agentContext)
|
|
629
|
-
const cbIdx = paveSource.indexOf("currentTask: 'stopped after ' + consecutiveFailures + ' consecutive failures',");
|
|
630
|
-
assert(cbIdx !== -1, 'should find circuit-breaker currentTask in writeStatus');
|
|
631
|
-
// Look backwards ~600 chars for the port: field in the same writeStatus call
|
|
632
|
-
const startIdx = Math.max(0, cbIdx - 600);
|
|
633
|
-
const section = paveSource.substring(startIdx, cbIdx);
|
|
634
|
-
const portLine = section.lastIndexOf('port:');
|
|
635
|
-
assert(portLine !== -1, 'should find port field in circuit-breaker writeStatus');
|
|
636
|
-
const portValue = section.substring(portLine, portLine + 30);
|
|
637
|
-
assert(portValue.indexOf('actualPort') !== -1,
|
|
638
|
-
'circuit-breaker writeStatus should use actualPort, not PORT');
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
runTest('index.js: SOUL-read failure backoff sleep uses actualPort', () => {
|
|
642
|
-
// When SOUL read fails but hasn't tripped the circuit breaker yet,
|
|
643
|
-
// the sleep status should use actualPort, not PORT
|
|
644
|
-
const soulErrorSleepIdx = paveSource.indexOf('signalAwareSleep(soulErrorSleepMs');
|
|
645
|
-
assert(soulErrorSleepIdx !== -1, 'should find soulErrorSleep call');
|
|
646
|
-
// Look backwards ~500 chars for the port: field in the writeStatus before sleep
|
|
647
|
-
const startIdx = Math.max(0, soulErrorSleepIdx - 500);
|
|
648
|
-
const section = paveSource.substring(startIdx, soulErrorSleepIdx);
|
|
649
|
-
const portLine = section.lastIndexOf('port:');
|
|
650
|
-
assert(portLine !== -1, 'should find port field before soulErrorSleep');
|
|
651
|
-
const portValue = section.substring(portLine, portLine + 30);
|
|
652
|
-
assert(portValue.indexOf('actualPort') !== -1,
|
|
653
|
-
'SOUL-read backoff writeStatus should use actualPort, not PORT');
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
runTest('index.js: unreliable port comment still exists as documentation', () => {
|
|
657
|
-
// The comment explaining the port discrepancy should still be present
|
|
658
|
-
assert(paveSource.indexOf('status.port is unreliable') !== -1 ||
|
|
659
|
-
paveSource.indexOf('actualPort') !== -1,
|
|
660
|
-
'should document the port behavior');
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
runTest('agent_spawn: awaits spawn confirmation with Promise pattern', () => {
|
|
664
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
665
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 7000);
|
|
666
|
-
assert(execSection.indexOf('await new Promise') !== -1,
|
|
667
|
-
'should await a Promise for spawn confirmation');
|
|
668
|
-
assert(execSection.indexOf("child.once('error'") !== -1,
|
|
669
|
-
'should listen for error event on child process');
|
|
670
|
-
assert(execSection.indexOf("child.once('spawn'") !== -1,
|
|
671
|
-
'should listen for spawn event on child process');
|
|
672
|
-
assert(execSection.indexOf('setTimeout') !== -1,
|
|
673
|
-
'should have a timeout fallback');
|
|
674
|
-
assert(execSection.indexOf('Agent spawn failed') !== -1,
|
|
675
|
-
'should return error message when spawn fails');
|
|
676
|
-
assert(execSection.indexOf('removeListener') !== -1,
|
|
677
|
-
'should remove listeners on settlement to prevent leaks');
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
runTest('agent_spawn: function is async for await support', () => {
|
|
681
|
-
assert(agentCommSource.indexOf('async function executeAgentSpawn(') !== -1,
|
|
682
|
-
'executeAgentSpawn should be declared async');
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
runTest('agent_spawn: dispatcher awaits executeAgentSpawn', () => {
|
|
686
|
-
const toolsSource = fs.readFileSync(
|
|
687
|
-
path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'index.js'), 'utf8');
|
|
688
|
-
const idx = toolsSource.indexOf('agent_spawn');
|
|
689
|
-
assert(idx !== -1, 'should find agent_spawn in tools/index.js');
|
|
690
|
-
const section = toolsSource.substring(idx, idx + 300);
|
|
691
|
-
assert(section.indexOf('await executeAgentSpawn') !== -1,
|
|
692
|
-
'dispatcher should await executeAgentSpawn to capture errors and duration correctly');
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
runTest('agent_spawn: handles isAgentAlive object return shape', () => {
|
|
696
|
-
const execIdx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
697
|
-
const execSection = agentCommSource.substring(execIdx, execIdx + 2000);
|
|
698
|
-
assert(execSection.indexOf('aliveResult') !== -1,
|
|
699
|
-
'should store isAgentAlive result in aliveResult');
|
|
700
|
-
assert(execSection.indexOf('.alive') !== -1,
|
|
701
|
-
'should extract .alive boolean from result');
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
runTest('agent-comm.test.js: uses agentComm.xxx not bare identifiers', () => {
|
|
705
|
-
const testSource = fs.readFileSync(
|
|
706
|
-
path.join(__dirname, 'agent-comm.test.js'), 'utf8');
|
|
707
|
-
// Line 65 should use agentComm.registerAgentComm, not bare registerAgentComm
|
|
708
|
-
const toolsTestIdx = testSource.indexOf('returns 3 tools when registered');
|
|
709
|
-
assert(toolsTestIdx !== -1, 'should find tool count test');
|
|
710
|
-
const testSection = testSource.substring(toolsTestIdx, toolsTestIdx + 500);
|
|
711
|
-
assert(testSection.indexOf('agentComm.registerAgentComm') !== -1,
|
|
712
|
-
'should use agentComm.registerAgentComm, not bare registerAgentComm');
|
|
713
|
-
assert(testSection.indexOf('agentComm.getAgentCommTools') !== -1,
|
|
714
|
-
'should use agentComm.getAgentCommTools, not bare getAgentCommTools');
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
// ============================================================
|
|
718
|
-
// 8. Status config persistence tests
|
|
719
|
-
// ============================================================
|
|
720
|
-
|
|
721
|
-
runTest('index.js: writeStatus stores soulPath', () => {
|
|
722
|
-
const statusIdx = paveSource.indexOf("state: registry.STATES.SLEEPING");
|
|
723
|
-
assert(statusIdx !== -1, 'should find SLEEPING writeStatus call');
|
|
724
|
-
const statusSection = paveSource.substring(statusIdx, statusIdx + 500);
|
|
725
|
-
assert(statusSection.indexOf('soulPath:') !== -1,
|
|
726
|
-
'writeStatus should include soulPath field');
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
runTest('index.js: writeStatus stores sleepMs', () => {
|
|
730
|
-
const statusIdx = paveSource.indexOf('state: registry.STATES.SLEEPING');
|
|
731
|
-
const statusSection = paveSource.substring(statusIdx, statusIdx + 500);
|
|
732
|
-
// Match shorthand `sleepMs,` or explicit `sleepMs:`
|
|
733
|
-
assert(statusSection.indexOf('sleepMs') !== -1,
|
|
734
|
-
'writeStatus should include sleepMs field');
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
runTest('index.js: writeStatus stores reinjectInterval', () => {
|
|
738
|
-
const statusIdx = paveSource.indexOf("state: registry.STATES.SLEEPING");
|
|
739
|
-
const statusSection = paveSource.substring(statusIdx, statusIdx + 500);
|
|
740
|
-
assert(statusSection.indexOf('reinjectInterval:') !== -1,
|
|
741
|
-
'writeStatus should include reinjectInterval field');
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
// ============================================================
|
|
745
|
-
// 9. _msToSleepArg helper function tests
|
|
746
|
-
// ============================================================
|
|
747
|
-
|
|
748
|
-
runTest('index.js: _msToSleepArg function exists', () => {
|
|
749
|
-
assert(paveSource.indexOf('function _msToSleepArg(') !== -1,
|
|
750
|
-
'should define _msToSleepArg function');
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
runTest('index.js: _msToSleepArg converts correctly', () => {
|
|
754
|
-
// Test the logic by inspecting the function body
|
|
755
|
-
const fnIdx = paveSource.indexOf('function _msToSleepArg(');
|
|
756
|
-
const fnSection = paveSource.substring(fnIdx, fnIdx + 400);
|
|
757
|
-
// Should handle hours, minutes, and seconds
|
|
758
|
-
assert(fnSection.indexOf("'h'") !== -1, 'should handle hours suffix');
|
|
759
|
-
assert(fnSection.indexOf("'m'") !== -1, 'should handle minutes suffix');
|
|
760
|
-
assert(fnSection.indexOf("'s'") !== -1, 'should handle seconds suffix');
|
|
761
|
-
// Should have default fallback
|
|
762
|
-
assert(fnSection.indexOf("'1m'") !== -1, 'should default to 1m for invalid/zero input');
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
// ============================================================
|
|
766
|
-
// 10. Help text and documentation tests
|
|
767
|
-
// ============================================================
|
|
768
|
-
|
|
769
|
-
runTest('index.js: help text includes restart subcommand', () => {
|
|
770
|
-
assert(paveSource.indexOf('pave agent restart [name]') !== -1,
|
|
771
|
-
'main help should mention restart subcommand');
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
runTest('index.js: restart help describes session preservation', () => {
|
|
775
|
-
// The restart subcommand description mentions preserving session
|
|
776
|
-
const restartHelpIdx = paveSource.indexOf('Restart a running agent');
|
|
777
|
-
assert(restartHelpIdx !== -1, 'should find restart help text');
|
|
778
|
-
const helpSection = paveSource.substring(restartHelpIdx, restartHelpIdx + 300);
|
|
779
|
-
assert(helpSection.indexOf('session') !== -1 || helpSection.indexOf('Session') !== -1,
|
|
780
|
-
'restart help should mention session preservation');
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
runTest('index.js: examples include restart command', () => {
|
|
784
|
-
assert(paveSource.indexOf('pave agent restart github-watcher') !== -1,
|
|
785
|
-
'examples should include restart command');
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
// ============================================================
|
|
789
|
-
// 11. agent-comm.js module header updated
|
|
790
|
-
// ============================================================
|
|
791
|
-
|
|
792
|
-
runTest('agent-comm.js: module comment mentions agent_spawn', () => {
|
|
793
|
-
assert(agentCommSource.indexOf('agent_spawn') !== -1,
|
|
794
|
-
'module header should mention agent_spawn tool');
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// ============================================================
|
|
798
|
-
// 12. Functional: executeAgentSpawn without registration
|
|
799
|
-
// ============================================================
|
|
800
|
-
|
|
801
|
-
runTest('agent_spawn: returns error when not registered', () => {
|
|
802
|
-
const agentComm = require(path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'tools', 'agent-comm.js'));
|
|
803
|
-
const resultPromise = agentComm.executeAgentSpawn({ name: 'test', soul: '/tmp/test.md' }, 'test-1');
|
|
804
|
-
// Since function is now async, it returns a Promise.
|
|
805
|
-
assert(resultPromise instanceof Promise, 'should return a Promise (async function)');
|
|
806
|
-
// Verify the source has the early-return guard for unregistered state
|
|
807
|
-
assert(agentCommSource.indexOf("'agent_spawn tool is not available") !== -1 ||
|
|
808
|
-
agentCommSource.indexOf('not available') !== -1,
|
|
809
|
-
'should have not-available error for unregistered state');
|
|
810
|
-
// Verify the early-return produces { success: false, error: ... }
|
|
811
|
-
const earlyReturnIdx = agentCommSource.indexOf('not available');
|
|
812
|
-
assert(earlyReturnIdx !== -1, 'should find not-available guard');
|
|
813
|
-
const earlySection = agentCommSource.substring(earlyReturnIdx - 100, earlyReturnIdx + 200);
|
|
814
|
-
assert(earlySection.indexOf('success: false') !== -1,
|
|
815
|
-
'early return should have success: false');
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
runTest('agent_spawn: JSDoc @returns shows Promise type', () => {
|
|
819
|
-
const idx = agentCommSource.indexOf('function executeAgentSpawn(');
|
|
820
|
-
assert(idx !== -1, 'should find executeAgentSpawn');
|
|
821
|
-
const docSection = agentCommSource.substring(Math.max(0, idx - 300), idx);
|
|
822
|
-
assert(docSection.indexOf('Promise') !== -1,
|
|
823
|
-
'@returns JSDoc should mention Promise for async function');
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
// ============================================================
|
|
827
|
-
// 13. Daemon preliminary status write tests (Issue #286)
|
|
828
|
-
// ============================================================
|
|
829
|
-
|
|
830
|
-
// Helper: extract daemon section from paveSource with guard assertion
|
|
831
|
-
function getDaemonSection(size) {
|
|
832
|
-
const daemonIdx = paveSource.indexOf('if (args.daemon)');
|
|
833
|
-
assert(daemonIdx !== -1, 'should have daemon mode check (if (args.daemon))');
|
|
834
|
-
return paveSource.substring(daemonIdx, daemonIdx + (size || 5000));
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Helper: extract the writeStatus call section within the daemon block
|
|
838
|
-
function getWriteStatusSection(daemonSection, size) {
|
|
839
|
-
const writeIdx = daemonSection.indexOf('registry.writeStatus(agentName');
|
|
840
|
-
assert(writeIdx !== -1, 'should have registry.writeStatus(agentName in daemon block');
|
|
841
|
-
return daemonSection.substring(writeIdx, writeIdx + (size || 400));
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
runTest('index.js: daemon writes preliminary status after spawn', () => {
|
|
845
|
-
const daemonSection = getDaemonSection();
|
|
846
|
-
assert(daemonSection.indexOf('registry.writeStatus(agentName') !== -1,
|
|
847
|
-
'daemon branch should call registry.writeStatus');
|
|
848
|
-
assert(daemonSection.indexOf('daemon spawning...') !== -1,
|
|
849
|
-
'should set currentTask to daemon spawning...');
|
|
850
|
-
// Verify ordering: writeStatus must come AFTER spawn (not before)
|
|
851
|
-
const spawnIdx = daemonSection.indexOf('spawn(process.execPath');
|
|
852
|
-
const writeIdx = daemonSection.indexOf('registry.writeStatus(agentName');
|
|
853
|
-
assert(spawnIdx !== -1, 'should have spawn call');
|
|
854
|
-
assert(writeIdx !== -1, 'should have writeStatus call');
|
|
855
|
-
assert(spawnIdx < writeIdx,
|
|
856
|
-
'writeStatus should come AFTER spawn (needs child.pid)');
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
runTest('index.js: daemon preliminary status uses same agent name as child', () => {
|
|
860
|
-
const daemonSection = getDaemonSection();
|
|
861
|
-
assert(daemonSection.indexOf("args.name || 'PAVE Agent'") !== -1,
|
|
862
|
-
'daemon should use args.name || \'PAVE Agent\' to match child logic');
|
|
863
|
-
assert(daemonSection.indexOf('path.basename(soulPath') === -1,
|
|
864
|
-
'daemon should NOT derive name from SOUL path basename');
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
runTest('index.js: daemon preliminary status uses registry.STATES constant', () => {
|
|
868
|
-
const daemonSection = getDaemonSection();
|
|
869
|
-
const writeSection = getWriteStatusSection(daemonSection);
|
|
870
|
-
assert(writeSection.indexOf('state: registry.STATES.STARTING') !== -1,
|
|
871
|
-
'should use registry.STATES.STARTING constant, not string literal');
|
|
872
|
-
assert(writeSection.indexOf("state: 'starting'") === -1,
|
|
873
|
-
'should NOT use string literal starting');
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
runTest('index.js: daemon preliminary status uses relative config path', () => {
|
|
877
|
-
const daemonSection = getDaemonSection();
|
|
878
|
-
const writeSection = getWriteStatusSection(daemonSection);
|
|
879
|
-
assert(writeSection.indexOf("config: args.config || '.pave'") !== -1,
|
|
880
|
-
'should use relative config path (args.config || .pave) matching child convention');
|
|
881
|
-
assert(writeSection.indexOf('config: configDir') === -1,
|
|
882
|
-
'should NOT use absolute configDir');
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
runTest('index.js: daemon preliminary status is wrapped in try/catch (best-effort)', () => {
|
|
886
|
-
const daemonSection = getDaemonSection();
|
|
887
|
-
const prelimIdx = daemonSection.indexOf('preliminary status');
|
|
888
|
-
assert(prelimIdx !== -1, 'should have preliminary status comment');
|
|
889
|
-
const tryIdx = daemonSection.indexOf('try {', prelimIdx);
|
|
890
|
-
const writeIdx = daemonSection.indexOf('registry.writeStatus(agentName');
|
|
891
|
-
const catchIdx = daemonSection.indexOf('catch (statusErr)');
|
|
892
|
-
assert(tryIdx !== -1 && writeIdx !== -1 && catchIdx !== -1,
|
|
893
|
-
'writeStatus should be wrapped in try/catch');
|
|
894
|
-
assert(tryIdx < writeIdx && writeIdx < catchIdx,
|
|
895
|
-
'try should come before writeStatus, which should come before catch');
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
runTest('index.js: daemon preliminary status includes required fields', () => {
|
|
899
|
-
const daemonSection = getDaemonSection();
|
|
900
|
-
const writeSection = getWriteStatusSection(daemonSection);
|
|
901
|
-
assert(writeSection.indexOf('pid: child.pid') !== -1, 'should include pid');
|
|
902
|
-
assert(writeSection.indexOf('cwd: process.cwd()') !== -1, 'should include cwd');
|
|
903
|
-
assert(writeSection.indexOf('startedAt: Date.now()') !== -1, 'should include startedAt');
|
|
904
|
-
assert(writeSection.indexOf('iteration: 0') !== -1, 'should include iteration');
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
runTest('index.js: daemon pre-flight checks liveness before spawning', () => {
|
|
908
|
-
const daemonSection = getDaemonSection();
|
|
909
|
-
// Should check isAgentAlive BEFORE spawn() to prevent unnecessary child
|
|
910
|
-
const aliveIdx = daemonSection.indexOf('isAgentAlive(agentName)');
|
|
911
|
-
assert(aliveIdx !== -1,
|
|
912
|
-
'should call isAgentAlive in daemon block');
|
|
913
|
-
const spawnIdx = daemonSection.indexOf('spawn(process.execPath');
|
|
914
|
-
assert(spawnIdx !== -1, 'should have spawn call in daemon block');
|
|
915
|
-
// Liveness check must come BEFORE spawn
|
|
916
|
-
assert(aliveIdx < spawnIdx,
|
|
917
|
-
'isAgentAlive check should come BEFORE spawn() to avoid unnecessary child');
|
|
918
|
-
// Should abort with non-zero on duplicate
|
|
919
|
-
const alreadyRunningIdx = daemonSection.indexOf('already running');
|
|
920
|
-
assert(alreadyRunningIdx !== -1,
|
|
921
|
-
'should report already running error');
|
|
922
|
-
// The 'return 1' must come AFTER the 'already running' error and BEFORE spawn
|
|
923
|
-
const sectionBetween = daemonSection.substring(alreadyRunningIdx, spawnIdx);
|
|
924
|
-
assert(sectionBetween.indexOf('return 1') !== -1,
|
|
925
|
-
'should return 1 between already-running error and spawn call');
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// ============================================================
|
|
929
|
-
// Summary
|
|
930
|
-
// ============================================================
|
|
931
|
-
|
|
932
|
-
console.log('\nTotal: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
|
|
933
|
-
if (failed > 0) process.exitCode = 1;
|