@cnrai/pave 0.3.35 → 0.3.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -218
  3. package/package.json +32 -35
  4. package/pave.js +3 -0
  5. package/sandbox/SandboxRunner.js +1 -0
  6. package/sandbox/pave-run.js +2 -0
  7. package/sandbox/permission.js +1 -0
  8. package/sandbox/utils/yaml.js +1 -0
  9. package/MARKETPLACE.md +0 -406
  10. package/build-binary.js +0 -591
  11. package/build-npm.js +0 -537
  12. package/build.js +0 -230
  13. package/check-binary.js +0 -26
  14. package/deploy.sh +0 -95
  15. package/index.js +0 -5776
  16. package/lib/agent-registry.js +0 -1037
  17. package/lib/args-parser.js +0 -837
  18. package/lib/blessed-widget-patched.js +0 -93
  19. package/lib/cli-markdown.js +0 -590
  20. package/lib/compaction.js +0 -153
  21. package/lib/duration.js +0 -94
  22. package/lib/hash.js +0 -22
  23. package/lib/marketplace.js +0 -866
  24. package/lib/memory-config.js +0 -166
  25. package/lib/skill-manager.js +0 -891
  26. package/lib/soul.js +0 -31
  27. package/lib/tool-output-formatter.js +0 -180
  28. package/start-pave.sh +0 -149
  29. package/status.js +0 -271
  30. package/test/abort-stream.test.js +0 -445
  31. package/test/agent-auto-compaction.test.js +0 -552
  32. package/test/agent-comm-abort.test.js +0 -95
  33. package/test/agent-comm.test.js +0 -598
  34. package/test/agent-inbox.test.js +0 -576
  35. package/test/agent-init.test.js +0 -264
  36. package/test/agent-interrupt.test.js +0 -314
  37. package/test/agent-lifecycle.test.js +0 -520
  38. package/test/agent-log-files.test.js +0 -349
  39. package/test/agent-mode.manual-test.js +0 -392
  40. package/test/agent-parsing.test.js +0 -228
  41. package/test/agent-post-stream-idle.test.js +0 -762
  42. package/test/agent-registry.test.js +0 -359
  43. package/test/agent-rm.test.js +0 -442
  44. package/test/agent-spawn.test.js +0 -933
  45. package/test/agent-status-api.test.js +0 -624
  46. package/test/agent-update.test.js +0 -435
  47. package/test/args-parser.test.js +0 -391
  48. package/test/auto-compaction-chat.manual-test.js +0 -227
  49. package/test/auto-compaction.test.js +0 -941
  50. package/test/build-config.test.js +0 -120
  51. package/test/build-npm.test.js +0 -388
  52. package/test/chat-command.test.js +0 -137
  53. package/test/chat-leading-lines.test.js +0 -159
  54. package/test/config-flag.test.js +0 -272
  55. package/test/cursor-drift.test.js +0 -135
  56. package/test/debug-require.js +0 -23
  57. package/test/dir-migration.test.js +0 -323
  58. package/test/duration.test.js +0 -229
  59. package/test/ghostty-term.test.js +0 -202
  60. package/test/http500-backoff.test.js +0 -854
  61. package/test/integration.test.js +0 -86
  62. package/test/memory-guard-env.test.js +0 -220
  63. package/test/pr233-fixes.test.js +0 -259
  64. package/test/run-agent-init.js +0 -297
  65. package/test/run-all.js +0 -64
  66. package/test/run-config-flag.js +0 -159
  67. package/test/run-cursor-drift.js +0 -82
  68. package/test/run-session-path.js +0 -154
  69. package/test/run-tests.js +0 -643
  70. package/test/sandbox-redirect.test.js +0 -202
  71. package/test/session-path.test.js +0 -132
  72. package/test/shebang-strip.test.js +0 -241
  73. package/test/soul-reinject.test.js +0 -1027
  74. package/test/soul-reread.test.js +0 -281
  75. package/test/tool-output-formatter.test.js +0 -486
  76. package/test/tool-output-gating.test.js +0 -143
  77. package/test/tool-states.test.js +0 -167
  78. package/test/tools-flag.test.js +0 -65
  79. package/test/tui-attach.test.js +0 -1255
  80. package/test/tui-compaction.test.js +0 -354
  81. package/test/tui-wrap.test.js +0 -568
  82. package/test-binary.js +0 -52
  83. package/test-binary2.js +0 -36
@@ -1,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
- }