@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,435 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Tests for 'pave agent update' command (Issue #279)
|
|
4
|
-
*
|
|
5
|
-
* Verifies:
|
|
6
|
-
* - args-parser recognizes 'update' as agent subcommand
|
|
7
|
-
* - Flags --sleep, --reinject-interval, --soul are parsed correctly
|
|
8
|
-
* - parseArgs integration for --reinject-interval validation
|
|
9
|
-
* - soulPath guard logic (missing/present/provided)
|
|
10
|
-
* - Existing soul file validation before restart
|
|
11
|
-
* - Stop-before-write ordering (source + simulation)
|
|
12
|
-
* - Dispatch routing from main to handleAgentUpdate
|
|
13
|
-
* - Change summary generation
|
|
14
|
-
*
|
|
15
|
-
* Duration/soul validation details are covered in duration.test.js
|
|
16
|
-
* and soul-reread.test.js respectively.
|
|
17
|
-
*
|
|
18
|
-
* Run with: node test/agent-update.test.js
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
let passed = 0;
|
|
22
|
-
let failed = 0;
|
|
23
|
-
|
|
24
|
-
function runTest(name, fn) {
|
|
25
|
-
try {
|
|
26
|
-
fn();
|
|
27
|
-
console.log('\u2705 ' + name);
|
|
28
|
-
passed++;
|
|
29
|
-
} catch (e) {
|
|
30
|
-
console.log('\u274C ' + name + ': ' + e.message);
|
|
31
|
-
failed++;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function assert(cond, msg) {
|
|
36
|
-
if (!cond) throw new Error(msg || 'Assertion failed');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function assertEqual(actual, expected, msg) {
|
|
40
|
-
if (actual !== expected) {
|
|
41
|
-
throw new Error((msg || 'assertEqual') + ': expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Module Imports (fail fast if broken) ───
|
|
46
|
-
|
|
47
|
-
const parseArgs = require('../lib/args-parser').parseArgs;
|
|
48
|
-
const duration = require('../lib/duration');
|
|
49
|
-
|
|
50
|
-
const formatDuration = duration.formatDuration;
|
|
51
|
-
const validateSoulFile = require('../lib/soul').validateSoulFile;
|
|
52
|
-
|
|
53
|
-
// ─── Args Parser Tests ───
|
|
54
|
-
|
|
55
|
-
runTest('should recognize "update" as agent subcommand', () => {
|
|
56
|
-
const result = parseArgs(['agent', 'update', 'designer']);
|
|
57
|
-
assertEqual(result.command, 'agent', 'command');
|
|
58
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
59
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
runTest('should parse update with --sleep flag', () => {
|
|
63
|
-
const result = parseArgs(['agent', 'update', '--sleep', '1h', 'designer']);
|
|
64
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
65
|
-
assertEqual(result.sleep, '1h', 'sleep');
|
|
66
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
runTest('should parse update with --reinject-interval flag', () => {
|
|
70
|
-
const result = parseArgs(['agent', 'update', '--reinject-interval', '5', 'designer']);
|
|
71
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
72
|
-
assertEqual(result.reinjectInterval, 5, 'reinjectInterval');
|
|
73
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
runTest('should parse update with --soul flag', () => {
|
|
77
|
-
const result = parseArgs(['agent', 'update', '--soul', './new-soul.md', 'designer']);
|
|
78
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
79
|
-
assertEqual(result.soul, './new-soul.md', 'soul');
|
|
80
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
runTest('should parse update with multiple flags', () => {
|
|
84
|
-
const result = parseArgs(['agent', 'update', '--sleep', '1h', '--reinject-interval', '5', 'designer']);
|
|
85
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
86
|
-
assertEqual(result.sleep, '1h', 'sleep');
|
|
87
|
-
assertEqual(result.reinjectInterval, 5, 'reinjectInterval');
|
|
88
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
runTest('should parse update with all three flags', () => {
|
|
92
|
-
const result = parseArgs(['agent', 'update', '--sleep', '30m', '--reinject-interval', '3', '--soul', './custom.md', 'myagent']);
|
|
93
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
94
|
-
assertEqual(result.sleep, '30m', 'sleep');
|
|
95
|
-
assertEqual(result.reinjectInterval, 3, 'reinjectInterval');
|
|
96
|
-
assertEqual(result.soul, './custom.md', 'soul');
|
|
97
|
-
assertEqual(result.commandArgs[0], 'myagent', 'commandArgs[0]');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
runTest('should parse update with no flags (show config mode)', () => {
|
|
101
|
-
const result = parseArgs(['agent', 'update', 'designer']);
|
|
102
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
103
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
104
|
-
assertEqual(result.reinjectInterval, null, 'reinjectInterval should be null');
|
|
105
|
-
assertEqual(result.soul, null, 'soul should be null');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
runTest('should reject multiple positional args for update', () => {
|
|
109
|
-
let exitCode = null;
|
|
110
|
-
const errors = [];
|
|
111
|
-
const origExit = process.exit;
|
|
112
|
-
const origError = console.error;
|
|
113
|
-
process.exit = function (code) { exitCode = code; throw new Error('EXIT_' + code); };
|
|
114
|
-
console.error = function () { errors.push(Array.prototype.join.call(arguments, ' ')); };
|
|
115
|
-
try {
|
|
116
|
-
parseArgs(['agent', 'update', 'designer', 'extra-arg']);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (!e.message.startsWith('EXIT_')) throw e;
|
|
119
|
-
} finally {
|
|
120
|
-
process.exit = origExit;
|
|
121
|
-
console.error = origError;
|
|
122
|
-
}
|
|
123
|
-
assertEqual(exitCode, 1, 'should exit with code 1');
|
|
124
|
-
assert(errors.length > 0, 'should print error message');
|
|
125
|
-
assert(errors[0].indexOf('at most one agent name') >= 0, 'error should mention "at most one agent name"');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
runTest('should parse update with agent name before flags', () => {
|
|
129
|
-
const result = parseArgs(['agent', 'update', 'designer', '--sleep', '2h']);
|
|
130
|
-
assertEqual(result.agentSubcommand, 'update', 'agentSubcommand');
|
|
131
|
-
assertEqual(result.commandArgs[0], 'designer', 'commandArgs[0]');
|
|
132
|
-
assertEqual(result.sleep, '2h', 'sleep');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ─── Reinject Interval Args-Parser Integration Tests ───
|
|
136
|
-
|
|
137
|
-
runTest('parseArgs should parse valid --reinject-interval', () => {
|
|
138
|
-
const result = parseArgs(['agent', 'update', 'myagent', '--reinject-interval', '5']);
|
|
139
|
-
assertEqual(result.reinjectInterval, 5, 'should parse to number 5');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
runTest('parseArgs should parse --reinject-interval 1 (minimum)', () => {
|
|
143
|
-
const result = parseArgs(['agent', 'update', 'myagent', '--reinject-interval', '1']);
|
|
144
|
-
assertEqual(result.reinjectInterval, 1, 'should parse to number 1');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
runTest('parseArgs should reject --reinject-interval with non-integer', () => {
|
|
148
|
-
let exitCode = null;
|
|
149
|
-
const errors = [];
|
|
150
|
-
const origExit = process.exit;
|
|
151
|
-
const origError = console.error;
|
|
152
|
-
process.exit = function (code) { exitCode = code; throw new Error('EXIT_' + code); };
|
|
153
|
-
console.error = function () { errors.push(Array.prototype.join.call(arguments, ' ')); };
|
|
154
|
-
try {
|
|
155
|
-
parseArgs(['agent', 'update', 'myagent', '--reinject-interval', 'abc']);
|
|
156
|
-
} catch (e) {
|
|
157
|
-
if (!e.message.startsWith('EXIT_')) throw e;
|
|
158
|
-
} finally {
|
|
159
|
-
process.exit = origExit;
|
|
160
|
-
console.error = origError;
|
|
161
|
-
}
|
|
162
|
-
assertEqual(exitCode, 1, 'should exit with code 1 for non-integer');
|
|
163
|
-
assert(errors.length > 0, 'should print error');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
runTest('parseArgs should reject --reinject-interval 0', () => {
|
|
167
|
-
let exitCode = null;
|
|
168
|
-
const errors = [];
|
|
169
|
-
const origExit = process.exit;
|
|
170
|
-
const origError = console.error;
|
|
171
|
-
process.exit = function (code) { exitCode = code; throw new Error('EXIT_' + code); };
|
|
172
|
-
console.error = function () { errors.push(Array.prototype.join.call(arguments, ' ')); };
|
|
173
|
-
try {
|
|
174
|
-
parseArgs(['agent', 'update', 'myagent', '--reinject-interval', '0']);
|
|
175
|
-
} catch (e) {
|
|
176
|
-
if (!e.message.startsWith('EXIT_')) throw e;
|
|
177
|
-
} finally {
|
|
178
|
-
process.exit = origExit;
|
|
179
|
-
console.error = origError;
|
|
180
|
-
}
|
|
181
|
-
assertEqual(exitCode, 1, 'should exit with code 1 for zero');
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// ─── Change Summary Tests ───
|
|
185
|
-
|
|
186
|
-
runTest('should build correct change summary for sleep change', () => {
|
|
187
|
-
const changes = [];
|
|
188
|
-
changes.push('sleep: ' + formatDuration(60000) + ' \u2192 ' + formatDuration(3600000));
|
|
189
|
-
assertEqual(changes.length, 1, 'one change');
|
|
190
|
-
assert(changes[0].indexOf('\u2192') >= 0, 'should contain arrow');
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
runTest('should build correct change summary for multiple changes', () => {
|
|
194
|
-
const changes = [];
|
|
195
|
-
changes.push('sleep: ' + formatDuration(60000) + ' \u2192 ' + formatDuration(3600000));
|
|
196
|
-
changes.push('reinject-interval: 10 \u2192 5');
|
|
197
|
-
changes.push('soul: /old.md \u2192 /new.md');
|
|
198
|
-
assertEqual(changes.length, 3, 'three changes');
|
|
199
|
-
assert(changes[1].indexOf('reinject-interval') >= 0, 'should mention reinject-interval');
|
|
200
|
-
assert(changes[2].indexOf('soul') >= 0, 'should mention soul');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ─── hasAnyUpdate Detection Tests ───
|
|
204
|
-
// Verify the production code checks all three flags before deciding to show config.
|
|
205
|
-
|
|
206
|
-
runTest('source: hasAnyUpdate checks sleep, reinjectInterval, and soul', () => {
|
|
207
|
-
const fs = require('fs');
|
|
208
|
-
const indexSource = fs.readFileSync(
|
|
209
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
210
|
-
);
|
|
211
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
212
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
213
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
214
|
-
// Verify the handler checks all three flags
|
|
215
|
-
assert(handlerBody.indexOf('hasSleep') > -1, 'should check hasSleep');
|
|
216
|
-
assert(handlerBody.indexOf('hasReinject') > -1, 'should check hasReinject');
|
|
217
|
-
assert(handlerBody.indexOf('hasSoul') > -1, 'should check hasSoul');
|
|
218
|
-
assert(handlerBody.indexOf('hasAnyUpdate') > -1, 'should compute hasAnyUpdate');
|
|
219
|
-
// Verify it gates on hasAnyUpdate for show-config path
|
|
220
|
-
assert(handlerBody.indexOf('if (!hasAnyUpdate)') > -1, 'should gate on !hasAnyUpdate');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ─── soulPath Guard Tests ───
|
|
224
|
-
|
|
225
|
-
runTest('soulPath guard: should detect missing soulPath when status has none and --soul not provided', () => {
|
|
226
|
-
const status = { sleepMs: 60000, reinjectInterval: 10 };
|
|
227
|
-
let newSoulPath = status.soulPath;
|
|
228
|
-
const hasSoul = false;
|
|
229
|
-
if (hasSoul) { newSoulPath = '/some/path.md'; }
|
|
230
|
-
assert(!newSoulPath, 'newSoulPath should be falsy');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
runTest('soulPath guard: should pass when status has soulPath', () => {
|
|
234
|
-
const status = { sleepMs: 60000, reinjectInterval: 10, soulPath: '/existing.md' };
|
|
235
|
-
const newSoulPath = status.soulPath;
|
|
236
|
-
assert(newSoulPath, 'newSoulPath should be truthy');
|
|
237
|
-
assertEqual(newSoulPath, '/existing.md', 'should use existing soulPath');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
runTest('soulPath guard: should pass when --soul flag provides a new path', () => {
|
|
241
|
-
const status = { sleepMs: 60000, reinjectInterval: 10 };
|
|
242
|
-
let newSoulPath = status.soulPath;
|
|
243
|
-
const hasSoul = true;
|
|
244
|
-
if (hasSoul) { newSoulPath = '/new-soul.md'; }
|
|
245
|
-
assertEqual(newSoulPath, '/new-soul.md', 'should use --soul flag value');
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// ─── Existing Soul Validation Tests ───
|
|
249
|
-
|
|
250
|
-
runTest('existing soul validation: should reject missing soul file from status', () => {
|
|
251
|
-
const hasSoul = false;
|
|
252
|
-
// Use a guaranteed-nonexistent path under os.tmpdir() for cross-platform reliability
|
|
253
|
-
const newSoulPath = require('os').tmpdir() + '/nonexistent-soul-' + Date.now() + '-' + Math.random().toString(36).slice(2) + '.md';
|
|
254
|
-
if (!hasSoul) {
|
|
255
|
-
const result = validateSoulFile(newSoulPath);
|
|
256
|
-
assert(!result.valid, 'should reject non-existent soul file');
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
runTest('source: existing soul validation gated on !hasSoul', () => {
|
|
261
|
-
// Verify the production code only validates the existing soul file when
|
|
262
|
-
// --soul was NOT provided (hasSoul is false). When --soul is provided,
|
|
263
|
-
// validation already happened during flag parsing.
|
|
264
|
-
const fs = require('fs');
|
|
265
|
-
const indexSource = fs.readFileSync(
|
|
266
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
267
|
-
);
|
|
268
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
269
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
270
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
271
|
-
// There are two validateSoulFile calls: one for the --soul flag (hasSoul path),
|
|
272
|
-
// and one for the existing soul file (!hasSoul path). Find the !hasSoul-gated one
|
|
273
|
-
// by searching for the specific variable name 'existingSoulValidation'.
|
|
274
|
-
const existingIdx = handlerBody.indexOf('existingSoulValidation = validateSoulFile(');
|
|
275
|
-
assert(existingIdx > -1, 'should find existingSoulValidation = validateSoulFile(');
|
|
276
|
-
const preceding = handlerBody.substring(Math.max(0, existingIdx - 200), existingIdx);
|
|
277
|
-
assert(preceding.indexOf('if (!hasSoul)') > -1, 'existingSoulValidation should be gated on if (!hasSoul)');
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// ─── Production Source Verification Tests ───
|
|
281
|
-
|
|
282
|
-
runTest('production code updates status fields before writeStatus', () => {
|
|
283
|
-
const fs = require('fs');
|
|
284
|
-
const indexSource = fs.readFileSync(
|
|
285
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
286
|
-
);
|
|
287
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
288
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
289
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
290
|
-
const sleepAssign = handlerBody.indexOf('status.sleepMs = newSleepMs');
|
|
291
|
-
const riAssign = handlerBody.indexOf('status.reinjectInterval = newReinjectInterval');
|
|
292
|
-
const soulAssign = handlerBody.indexOf('status.soulPath = newSoulPath');
|
|
293
|
-
const writeCall = handlerBody.indexOf('registry.writeStatus(agentName');
|
|
294
|
-
assert(sleepAssign > -1, 'should assign status.sleepMs');
|
|
295
|
-
assert(riAssign > -1, 'should assign status.reinjectInterval');
|
|
296
|
-
assert(soulAssign > -1, 'should assign status.soulPath');
|
|
297
|
-
assert(writeCall > -1, 'should call writeStatus');
|
|
298
|
-
assert(sleepAssign < writeCall, 'sleepMs before writeStatus');
|
|
299
|
-
assert(riAssign < writeCall, 'reinjectInterval before writeStatus');
|
|
300
|
-
assert(soulAssign < writeCall, 'soulPath before writeStatus');
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
runTest('production code checks liveness before writing status', () => {
|
|
304
|
-
const fs = require('fs');
|
|
305
|
-
const indexSource = fs.readFileSync(
|
|
306
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
307
|
-
);
|
|
308
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
309
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
310
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
311
|
-
const aliveIdx = handlerBody.indexOf('registry.isAgentAlive(');
|
|
312
|
-
const writeIdx = handlerBody.indexOf('registry.writeStatus(agentName');
|
|
313
|
-
assert(aliveIdx > -1, 'should find isAgentAlive call');
|
|
314
|
-
assert(writeIdx > -1, 'should find writeStatus(agentName call');
|
|
315
|
-
assert(aliveIdx < writeIdx, 'isAgentAlive before writeStatus');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
runTest('main dispatch routes agent update to handleAgentUpdate', () => {
|
|
319
|
-
const fs = require('fs');
|
|
320
|
-
const indexSource = fs.readFileSync(
|
|
321
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
322
|
-
);
|
|
323
|
-
const dispatchIdx = indexSource.indexOf("args.agentSubcommand === 'update'");
|
|
324
|
-
assert(dispatchIdx > -1, 'should have dispatch check for update subcommand');
|
|
325
|
-
const dispatchBlock = indexSource.substring(dispatchIdx, dispatchIdx + 200);
|
|
326
|
-
assert(dispatchBlock.indexOf('handleAgentUpdate(args)') > -1,
|
|
327
|
-
'dispatch should call handleAgentUpdate(args)');
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// ─── Stop-Before-Write Ordering Tests ───
|
|
331
|
-
// Source-based: verify the production code calls stopAgent before writeStatus
|
|
332
|
-
// when the agent is alive (the running-agent path).
|
|
333
|
-
|
|
334
|
-
runTest('source: stopAgent called before writeStatus when agent is alive', () => {
|
|
335
|
-
const fs = require('fs');
|
|
336
|
-
const indexSource = fs.readFileSync(
|
|
337
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
338
|
-
);
|
|
339
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
340
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
341
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
342
|
-
const stopIdx = handlerBody.indexOf('registry.stopAgent(');
|
|
343
|
-
const writeIdx = handlerBody.indexOf('registry.writeStatus(agentName');
|
|
344
|
-
assert(stopIdx > -1, 'should find stopAgent call');
|
|
345
|
-
assert(writeIdx > -1, 'should find writeStatus call');
|
|
346
|
-
assert(stopIdx < writeIdx, 'stopAgent must come before writeStatus in source');
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
runTest('source: readStatus called after stopAgent (re-read fresh status)', () => {
|
|
350
|
-
const fs = require('fs');
|
|
351
|
-
const indexSource = fs.readFileSync(
|
|
352
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
353
|
-
);
|
|
354
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
355
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
356
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
357
|
-
const stopIdx = handlerBody.indexOf('registry.stopAgent(');
|
|
358
|
-
// Find the re-read after stop (second readStatus call)
|
|
359
|
-
const firstRead = handlerBody.indexOf('registry.readStatus(');
|
|
360
|
-
const secondRead = handlerBody.indexOf('registry.readStatus(', firstRead + 1);
|
|
361
|
-
assert(secondRead > -1, 'should find a second readStatus call (re-read after stop)');
|
|
362
|
-
assert(secondRead > stopIdx, 'second readStatus should come after stopAgent');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// ─── No-Op Detection Tests ───
|
|
366
|
-
// Verify that handleAgentUpdate skips restart when values haven't changed.
|
|
367
|
-
|
|
368
|
-
runTest('source: no-op detection returns early when all values unchanged', () => {
|
|
369
|
-
const fs = require('fs');
|
|
370
|
-
const indexSource = fs.readFileSync(
|
|
371
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
372
|
-
);
|
|
373
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
374
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
375
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
376
|
-
// Verify the handler computes unchanged flags
|
|
377
|
-
assert(handlerBody.indexOf('sleepUnchanged') > -1, 'should compute sleepUnchanged');
|
|
378
|
-
assert(handlerBody.indexOf('reinjectUnchanged') > -1, 'should compute reinjectUnchanged');
|
|
379
|
-
assert(handlerBody.indexOf('soulUnchanged') > -1, 'should compute soulUnchanged');
|
|
380
|
-
// Verify it returns early when all unchanged
|
|
381
|
-
assert(handlerBody.indexOf('No changes needed') > -1, 'should have no-changes-needed message');
|
|
382
|
-
// Verify no-op check comes before writeStatus
|
|
383
|
-
const noopIdx = handlerBody.indexOf('No changes needed');
|
|
384
|
-
const writeIdx = handlerBody.indexOf('registry.writeStatus(agentName');
|
|
385
|
-
assert(noopIdx < writeIdx, 'no-op check should come before writeStatus');
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// ─── Restart Race Guard Tests ───
|
|
389
|
-
// Verify that handleAgentUpdate re-checks liveness after stopAgent to detect
|
|
390
|
-
// if another process restarted the agent during the stop window.
|
|
391
|
-
|
|
392
|
-
runTest('source: re-checks liveness after stopAgent before writing', () => {
|
|
393
|
-
const fs = require('fs');
|
|
394
|
-
const indexSource = fs.readFileSync(
|
|
395
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
396
|
-
);
|
|
397
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
398
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate in source');
|
|
399
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 11000);
|
|
400
|
-
const stopIdx = handlerBody.indexOf('registry.stopAgent(');
|
|
401
|
-
// Find the post-stop liveness re-check
|
|
402
|
-
const postStopAlive = handlerBody.indexOf('postStopAlive');
|
|
403
|
-
assert(postStopAlive > -1, 'should have postStopAlive variable');
|
|
404
|
-
assert(postStopAlive > stopIdx, 'postStopAlive check should come after stopAgent');
|
|
405
|
-
const writeIdx = handlerBody.indexOf('registry.writeStatus(agentName');
|
|
406
|
-
assert(postStopAlive < writeIdx, 'postStopAlive check should come before writeStatus');
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// ─── Agent Name Inference Tests ───
|
|
410
|
-
// Verify that handleAgentUpdate's inference matches handleAgentStop:
|
|
411
|
-
// prefer alive agents, fall back to stale only when no alive match.
|
|
412
|
-
|
|
413
|
-
runTest('source: agent name inference prefers alive agents over stale', () => {
|
|
414
|
-
const fs = require('fs');
|
|
415
|
-
const indexSource = fs.readFileSync(
|
|
416
|
-
require('path').join(__dirname, '..', 'index.js'), 'utf8',
|
|
417
|
-
);
|
|
418
|
-
const handleIdx = indexSource.indexOf('async function handleAgentUpdate(');
|
|
419
|
-
assert(handleIdx > -1, 'should find handleAgentUpdate');
|
|
420
|
-
const handlerBody = indexSource.substring(handleIdx, handleIdx + 3000);
|
|
421
|
-
// Verify the handler uses alive/stale split (not flat cwdMatches)
|
|
422
|
-
assert(handlerBody.indexOf('aliveMatches') > -1, 'should use aliveMatches array');
|
|
423
|
-
assert(handlerBody.indexOf('staleMatches') > -1, 'should use staleMatches array');
|
|
424
|
-
// Verify alive check uses same pattern as handleAgentStop
|
|
425
|
-
assert(handlerBody.indexOf("state === 'working'") > -1, 'should check working state');
|
|
426
|
-
assert(handlerBody.indexOf("state === 'sleeping'") > -1, 'should check sleeping state');
|
|
427
|
-
assert(handlerBody.indexOf("state === 'starting'") > -1, 'should check starting state');
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// ─── Print Results ───
|
|
431
|
-
|
|
432
|
-
console.log('\n' + passed + ' passed, ' + failed + ' failed, ' + (passed + failed) + ' total');
|
|
433
|
-
if (failed > 0) {
|
|
434
|
-
process.exitCode = 1;
|
|
435
|
-
}
|