@cnrai/pave 0.3.32 → 0.3.34

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/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. package/sandbox/utils/yaml.js +0 -1
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for agent inbox protocol (Issue #200)
4
+ *
5
+ * Verifies inbox message writing, draining, formatting, and
6
+ * args-parser support for -a/--agent flag.
7
+ *
8
+ * Designed to run standalone: node test/agent-inbox.test.js
9
+ * Imports modules directly (not mocked) for integration-level testing.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ const registry = require('../lib/agent-registry');
17
+ const parseArgs = require('../lib/args-parser').parseArgs;
18
+
19
+ let passed = 0;
20
+ let failed = 0;
21
+
22
+ // Use temp dir for all tests
23
+ const testDir = path.join(os.tmpdir(), 'agent-inbox-test-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6));
24
+ let canMkdir = false;
25
+ try {
26
+ if (typeof fs.mkdirSync === 'function') {
27
+ fs.mkdirSync(testDir, { recursive: true });
28
+ canMkdir = true;
29
+ }
30
+ } catch (e) {}
31
+ if (canMkdir) registry.setAgentsDir(testDir);
32
+
33
+ function assert(cond, msg) {
34
+ if (!cond) throw new Error(msg || 'assertion failed');
35
+ }
36
+
37
+ function runTest(name, fn) {
38
+ try {
39
+ fn();
40
+ console.log('[PASS] ' + name);
41
+ passed++;
42
+ } catch (e) {
43
+ console.log('[FAIL] ' + name + ': ' + e.message);
44
+ failed++;
45
+ }
46
+ }
47
+
48
+ function resetTestDir() {
49
+ if (!canMkdir) return;
50
+ try { fs.rmSync(testDir, { recursive: true, force: true }); } catch (e) {}
51
+ fs.mkdirSync(testDir, { recursive: true });
52
+ }
53
+
54
+ function cleanup() {
55
+ registry.resetAgentsDir();
56
+ try { fs.rmSync(testDir, { recursive: true, force: true }); } catch (e) {}
57
+ }
58
+
59
+ // ============================================================
60
+ // getInboxDir
61
+ // ============================================================
62
+
63
+ runTest('getInboxDir returns correct path', () => {
64
+ const dir = registry.getInboxDir('my-agent');
65
+ assert(dir.indexOf('my-agent') !== -1, 'should contain agent name');
66
+ assert(dir.endsWith('inbox'), 'should end with inbox');
67
+ });
68
+
69
+ // ============================================================
70
+ // writeInboxMessage
71
+ // ============================================================
72
+
73
+ resetTestDir();
74
+
75
+ runTest('writeInboxMessage: writes a message file', () => {
76
+ const result = registry.writeInboxMessage('test-agent', 'hello world');
77
+ assert(result.success === true, 'should succeed');
78
+ assert(result.error === null, 'should have no error');
79
+ assert(result.file !== null, 'should return filename');
80
+ assert(result.file.endsWith('.json'), 'filename should end with .json');
81
+
82
+ // Verify file exists and is valid JSON
83
+ const inboxDir = registry.getInboxDir('test-agent');
84
+ const content = JSON.parse(fs.readFileSync(path.join(inboxDir, result.file), 'utf8'));
85
+ assert(content.message === 'hello world', 'message should match');
86
+ assert(content.from === 'tui', 'default from should be tui');
87
+ assert(content.priority === 'normal', 'default priority should be normal');
88
+ assert(typeof content.timestamp === 'number', 'should have timestamp');
89
+ });
90
+
91
+ runTest('writeInboxMessage: respects from and priority options', () => {
92
+ const result = registry.writeInboxMessage('test-agent', 'urgent task', {
93
+ from: 'code-reviewer',
94
+ priority: 'high',
95
+ });
96
+ assert(result.success === true, 'should succeed');
97
+
98
+ const inboxDir = registry.getInboxDir('test-agent');
99
+ const content = JSON.parse(fs.readFileSync(path.join(inboxDir, result.file), 'utf8'));
100
+ assert(content.from === 'code-reviewer', 'from should match');
101
+ assert(content.priority === 'high', 'priority should match');
102
+ });
103
+
104
+ runTest('writeInboxMessage: rejects empty message', () => {
105
+ const result = registry.writeInboxMessage('test-agent', '');
106
+ assert(result.success === false, 'should fail');
107
+ assert(result.error !== null, 'should have error');
108
+ });
109
+
110
+ runTest('writeInboxMessage: rejects non-string message', () => {
111
+ const result = registry.writeInboxMessage('test-agent', 123);
112
+ assert(result.success === false, 'should fail');
113
+ assert(result.error !== null, 'should have error');
114
+ });
115
+
116
+ runTest('writeInboxMessage: rejects invalid priority', () => {
117
+ const result = registry.writeInboxMessage('test-agent', 'test', { priority: 'critical' });
118
+ assert(result.success === false, 'should fail');
119
+ assert(result.error.indexOf('Invalid priority') !== -1, 'should mention invalid priority');
120
+ });
121
+
122
+ runTest('writeInboxMessage: sequence numbers increment', () => {
123
+ resetTestDir();
124
+ const r1 = registry.writeInboxMessage('seq-agent', 'msg 1');
125
+ const r2 = registry.writeInboxMessage('seq-agent', 'msg 2');
126
+ const r3 = registry.writeInboxMessage('seq-agent', 'msg 3');
127
+
128
+ assert(r1.success && r2.success && r3.success, 'all should succeed');
129
+
130
+ // Extract sequence numbers
131
+ const seq1 = parseInt(r1.file.split('-')[0], 10);
132
+ const seq2 = parseInt(r2.file.split('-')[0], 10);
133
+ const seq3 = parseInt(r3.file.split('-')[0], 10);
134
+
135
+ assert(seq1 < seq2, 'seq2 > seq1');
136
+ assert(seq2 < seq3, 'seq3 > seq2');
137
+ });
138
+
139
+ runTest('writeInboxMessage: filenames are zero-padded for FIFO sort', () => {
140
+ resetTestDir();
141
+ const r1 = registry.writeInboxMessage('pad-agent', 'msg');
142
+ assert(r1.file.match(/^\d{4}-/), 'filename should start with 4-digit zero-padded sequence');
143
+ // Filename should also include unique suffix: NNNN-<timestamp>-<pid>-<random>.json
144
+ assert(r1.file.match(/^\d{4}-\d+-\d+-[0-9a-f]+\.json$/), 'filename should match full pattern with unique suffix');
145
+ });
146
+
147
+ // ============================================================
148
+ // inboxHasMessages
149
+ // ============================================================
150
+
151
+ resetTestDir();
152
+
153
+ runTest('inboxHasMessages: false when no inbox directory', () => {
154
+ assert(registry.inboxHasMessages('nonexistent') === false, 'should be false');
155
+ });
156
+
157
+ runTest('inboxHasMessages: false when inbox is empty', () => {
158
+ const inboxDir = registry.getInboxDir('empty-agent');
159
+ fs.mkdirSync(inboxDir, { recursive: true });
160
+ assert(registry.inboxHasMessages('empty-agent') === false, 'should be false');
161
+ });
162
+
163
+ runTest('inboxHasMessages: true when messages exist', () => {
164
+ registry.writeInboxMessage('has-msg-agent', 'test');
165
+ assert(registry.inboxHasMessages('has-msg-agent') === true, 'should be true');
166
+ });
167
+
168
+ runTest('inboxHasMessages: ignores .tmp files', () => {
169
+ const inboxDir = registry.getInboxDir('tmp-agent');
170
+ fs.mkdirSync(inboxDir, { recursive: true });
171
+ fs.writeFileSync(path.join(inboxDir, '0001-123.json.tmp'), '{}');
172
+ assert(registry.inboxHasMessages('tmp-agent') === false, 'should be false (only .tmp files)');
173
+ });
174
+
175
+ // ============================================================
176
+ // drainInbox
177
+ // ============================================================
178
+
179
+ resetTestDir();
180
+
181
+ runTest('drainInbox: returns empty when no messages', () => {
182
+ const result = registry.drainInbox('no-inbox');
183
+ assert(result.messages.length === 0, 'should have no messages');
184
+ assert(result.remaining === 0, 'should have no remaining');
185
+ });
186
+
187
+ runTest('drainInbox: error path returns files array', () => {
188
+ // When inbox dir does not exist, drainInbox should return files: []
189
+ // so callers using result.files (e.g. deleteInboxFiles) don't crash
190
+ const result = registry.drainInbox('nonexistent-agent-xyz');
191
+ assert(Array.isArray(result.files), 'files should be an array, got: ' + typeof result.files);
192
+ assert(result.files.length === 0, 'files should be empty');
193
+ assert(result.messages.length === 0, 'messages should be empty');
194
+ assert(result.remaining === 0, 'remaining should be 0');
195
+ });
196
+
197
+ runTest('drainInbox: reads and deletes messages', () => {
198
+ resetTestDir();
199
+ registry.writeInboxMessage('drain-agent', 'msg 1');
200
+ registry.writeInboxMessage('drain-agent', 'msg 2');
201
+
202
+ const result = registry.drainInbox('drain-agent');
203
+ assert(result.messages.length === 2, 'should have 2 messages');
204
+ assert(result.remaining === 0, 'should have no remaining');
205
+
206
+ // Messages should be deleted
207
+ assert(registry.inboxHasMessages('drain-agent') === false, 'inbox should be empty after drain');
208
+ });
209
+
210
+ runTest('drainInbox: respects batch limit', () => {
211
+ resetTestDir();
212
+ for (let i = 0; i < 8; i++) {
213
+ registry.writeInboxMessage('limit-agent', 'msg ' + i);
214
+ }
215
+
216
+ const result = registry.drainInbox('limit-agent', { limit: 5 });
217
+ assert(result.messages.length === 5, 'should drain 5 messages');
218
+ assert(result.remaining === 3, 'should have 3 remaining');
219
+
220
+ // Remaining messages should still be in inbox
221
+ assert(registry.inboxHasMessages('limit-agent') === true, 'inbox should still have messages');
222
+ });
223
+
224
+ runTest('drainInbox: FIFO ordering by filename', () => {
225
+ resetTestDir();
226
+ registry.writeInboxMessage('fifo-agent', 'first');
227
+ registry.writeInboxMessage('fifo-agent', 'second');
228
+ registry.writeInboxMessage('fifo-agent', 'third');
229
+
230
+ const result = registry.drainInbox('fifo-agent');
231
+ // Within same priority, order should be FIFO
232
+ assert(result.messages[0].message === 'first', 'first message should be first');
233
+ assert(result.messages[1].message === 'second', 'second message should be second');
234
+ assert(result.messages[2].message === 'third', 'third message should be third');
235
+ });
236
+
237
+ runTest('drainInbox: high-priority messages sorted before normal', () => {
238
+ resetTestDir();
239
+ registry.writeInboxMessage('priority-agent', 'normal 1', { priority: 'normal' });
240
+ registry.writeInboxMessage('priority-agent', 'high 1', { priority: 'high' });
241
+ registry.writeInboxMessage('priority-agent', 'normal 2', { priority: 'normal' });
242
+ registry.writeInboxMessage('priority-agent', 'high 2', { priority: 'high' });
243
+
244
+ const result = registry.drainInbox('priority-agent');
245
+ assert(result.messages.length === 4, 'should have 4 messages');
246
+ assert(result.messages[0].priority === 'high', 'first should be high priority');
247
+ assert(result.messages[1].priority === 'high', 'second should be high priority');
248
+ assert(result.messages[2].priority === 'normal', 'third should be normal priority');
249
+ assert(result.messages[3].priority === 'normal', 'fourth should be normal priority');
250
+ });
251
+
252
+ runTest('drainInbox: handles malformed JSON gracefully', () => {
253
+ resetTestDir();
254
+ const inboxDir = registry.getInboxDir('malformed-agent');
255
+ fs.mkdirSync(inboxDir, { recursive: true });
256
+ fs.writeFileSync(path.join(inboxDir, '0001-1234.json'), 'not json');
257
+ registry.writeInboxMessage('malformed-agent', 'valid msg');
258
+
259
+ // Suppress console.error during this test
260
+ const origErr = console.error;
261
+ const errMessages = [];
262
+ console.error = function (msg) { errMessages.push(msg); };
263
+
264
+ const result = registry.drainInbox('malformed-agent');
265
+ console.error = origErr;
266
+
267
+ assert(result.messages.length === 1, 'should have 1 valid message');
268
+ assert(result.messages[0].message === 'valid msg', 'should be the valid message');
269
+ // Malformed file should be deleted
270
+ const remaining = fs.readdirSync(inboxDir).filter((f) => { return f.endsWith('.json'); });
271
+ assert(remaining.length === 0, 'malformed file should be deleted');
272
+ });
273
+
274
+ runTest('drainInbox: handles missing message field gracefully', () => {
275
+ resetTestDir();
276
+ const inboxDir = registry.getInboxDir('nomsg-agent');
277
+ fs.mkdirSync(inboxDir, { recursive: true });
278
+ fs.writeFileSync(path.join(inboxDir, '0001-1234.json'), JSON.stringify({ from: 'test' }));
279
+
280
+ const origErr = console.error;
281
+ console.error = function () {};
282
+ const result = registry.drainInbox('nomsg-agent');
283
+ console.error = origErr;
284
+
285
+ assert(result.messages.length === 0, 'should have 0 valid messages');
286
+ });
287
+
288
+ // ============================================================
289
+ // formatInboxMessages
290
+ // ============================================================
291
+
292
+ runTest('formatInboxMessages: empty array returns empty string', () => {
293
+ assert(registry.formatInboxMessages([]) === '', 'should be empty');
294
+ assert(registry.formatInboxMessages(null) === '', 'null should be empty');
295
+ });
296
+
297
+ runTest('formatInboxMessages: formats single message', () => {
298
+ const formatted = registry.formatInboxMessages([
299
+ { message: 'hello', from: 'tui', priority: 'normal' },
300
+ ]);
301
+ assert(formatted.indexOf('You have received messages') !== -1, 'should have header');
302
+ assert(formatted.indexOf('[1]') !== -1, 'should have message number');
303
+ assert(formatted.indexOf('from: tui') !== -1, 'should show sender');
304
+ assert(formatted.indexOf('hello') !== -1, 'should contain message');
305
+ assert(formatted.indexOf('priority: high') === -1, 'should not show priority for normal');
306
+ });
307
+
308
+ runTest('formatInboxMessages: shows high priority tag', () => {
309
+ const formatted = registry.formatInboxMessages([
310
+ { message: 'urgent', from: 'admin', priority: 'high' },
311
+ ]);
312
+ assert(formatted.indexOf('priority: high') !== -1, 'should show priority for high');
313
+ });
314
+
315
+ runTest('formatInboxMessages: shows remaining count', () => {
316
+ const formatted = registry.formatInboxMessages([
317
+ { message: 'msg', from: 'tui', priority: 'normal' },
318
+ ], 3);
319
+ assert(formatted.indexOf('3 more messages remaining') !== -1, 'should show remaining');
320
+ });
321
+
322
+ runTest('formatInboxMessages: formats multiple messages', () => {
323
+ const formatted = registry.formatInboxMessages([
324
+ { message: 'first', from: 'tui', priority: 'normal' },
325
+ { message: 'second', from: 'bot', priority: 'high' },
326
+ ]);
327
+ assert(formatted.indexOf('[1]') !== -1, 'should have [1]');
328
+ assert(formatted.indexOf('[2]') !== -1, 'should have [2]');
329
+ assert(formatted.indexOf('first') !== -1, 'should have first message');
330
+ assert(formatted.indexOf('second') !== -1, 'should have second message');
331
+ });
332
+
333
+ // ============================================================
334
+ // args-parser: -a / --agent flag
335
+ // ============================================================
336
+
337
+ runTest('args-parser: --agent flag for chat command', () => {
338
+ const result = parseArgs(['chat', '--agent', 'my-agent', 'hello world']);
339
+ assert(result.command === 'chat', 'command should be chat');
340
+ assert(result.agent === 'my-agent', 'agent should be my-agent');
341
+ assert(result.commandArgs.join(' ') === 'hello world', 'message should be preserved');
342
+ });
343
+
344
+ runTest('args-parser: -a shorthand for chat command', () => {
345
+ const result = parseArgs(['chat', '-a', 'my-agent', 'hello']);
346
+ assert(result.command === 'chat', 'command should be chat');
347
+ assert(result.agent === 'my-agent', 'agent should be my-agent');
348
+ assert(result.commandArgs.join(' ') === 'hello', 'message should be preserved');
349
+ });
350
+
351
+ runTest('args-parser: agent flag not set by default', () => {
352
+ const result = parseArgs(['chat', 'hello']);
353
+ assert(result.agent === null, 'agent should be null by default');
354
+ });
355
+
356
+ runTest('args-parser: --agent works with other chat flags', () => {
357
+ const result = parseArgs(['chat', '--agent', 'test', '--no-stream', 'my message']);
358
+ assert(result.agent === 'test', 'agent should be test');
359
+ assert(result.noStream === true, 'noStream should be true');
360
+ assert(result.commandArgs.join(' ') === 'my message', 'message should be preserved');
361
+ });
362
+
363
+ runTest('args-parser: --agent as global flag', () => {
364
+ const result = parseArgs(['chat', 'some message', '--agent', 'global-test']);
365
+ assert(result.agent === 'global-test', 'agent should be global-test');
366
+ });
367
+
368
+ runTest('args-parser: -a does not set --all for chat command', () => {
369
+ const result = parseArgs(['chat', '-a', 'my-agent', 'hello']);
370
+ assert(result.agent === 'my-agent', 'agent should be my-agent');
371
+ assert(!result.all, '-a should not set all=true for chat command');
372
+ });
373
+
374
+ runTest('args-parser: -a still sets --all for non-chat commands', () => {
375
+ const result = parseArgs(['search', '-a', 'keyword']);
376
+ assert(result.all === true, '-a should set all=true for search command');
377
+ });
378
+
379
+ // ============================================================
380
+ // Numeric sort beyond zero-pad width
381
+ // ============================================================
382
+
383
+ resetTestDir();
384
+
385
+ runTest('drainInbox: numeric sort handles sequences beyond zero-pad width', () => {
386
+ const inboxDir = registry.getInboxDir('bigsort-agent');
387
+ fs.mkdirSync(inboxDir, { recursive: true });
388
+ // Write files simulating sequences > 9999
389
+ fs.writeFileSync(path.join(inboxDir, '9999-1000-x.json'), JSON.stringify({ message: 'older', from: 'test', priority: 'normal', timestamp: 1000 }));
390
+ fs.writeFileSync(path.join(inboxDir, '10000-1001-x.json'), JSON.stringify({ message: 'newer', from: 'test', priority: 'normal', timestamp: 1001 }));
391
+
392
+ const result = registry.drainInbox('bigsort-agent');
393
+ assert(result.messages.length === 2, 'should have 2 messages');
394
+ assert(result.messages[0].message === 'older', '9999 should come before 10000');
395
+ assert(result.messages[1].message === 'newer', '10000 should come after 9999');
396
+ });
397
+
398
+ runTest('writeInboxMessage: concurrent writes produce unique filenames', () => {
399
+ resetTestDir();
400
+ // Write multiple messages rapidly - each should get unique filename
401
+ const results = [];
402
+ for (let i = 0; i < 10; i++) {
403
+ results.push(registry.writeInboxMessage('concurrent-agent', 'msg ' + i));
404
+ }
405
+
406
+ const filenames = new Set();
407
+ for (let j = 0; j < results.length; j++) {
408
+ assert(results[j].success, 'write ' + j + ' should succeed');
409
+ assert(!filenames.has(results[j].file), 'filename should be unique: ' + results[j].file);
410
+ filenames.add(results[j].file);
411
+ }
412
+ assert(filenames.size === 10, 'should have 10 unique filenames');
413
+ });
414
+
415
+ // ============================================================
416
+ // Integration: write then drain
417
+ // ============================================================
418
+
419
+ resetTestDir();
420
+
421
+ runTest('integration: write 3 messages then drain all', () => {
422
+ registry.writeInboxMessage('int-agent', 'msg 1');
423
+ registry.writeInboxMessage('int-agent', 'msg 2', { priority: 'high' });
424
+ registry.writeInboxMessage('int-agent', 'msg 3');
425
+
426
+ assert(registry.inboxHasMessages('int-agent') === true, 'should have messages');
427
+
428
+ const result = registry.drainInbox('int-agent');
429
+ assert(result.messages.length === 3, 'should have 3 messages');
430
+ assert(result.messages[0].priority === 'high', 'high priority should be first');
431
+ assert(result.remaining === 0, 'should have no remaining');
432
+
433
+ assert(registry.inboxHasMessages('int-agent') === false, 'inbox should be empty');
434
+
435
+ // Format for LLM
436
+ const formatted = registry.formatInboxMessages(result.messages, result.remaining);
437
+ assert(formatted.indexOf('You have received messages') !== -1, 'should have header');
438
+ assert(formatted.indexOf('[1]') !== -1, 'should have message 1');
439
+ assert(formatted.indexOf('[2]') !== -1, 'should have message 2');
440
+ assert(formatted.indexOf('[3]') !== -1, 'should have message 3');
441
+ });
442
+
443
+ runTest('integration: stopped agent still accepts messages', () => {
444
+ resetTestDir();
445
+ // Write status for a stopped agent
446
+ registry.writeStatus('stopped-agent', {
447
+ state: registry.STATES.STOPPED,
448
+ pid: 99999,
449
+ cwd: '/tmp',
450
+ });
451
+
452
+ // Should still be able to write to inbox
453
+ const result = registry.writeInboxMessage('stopped-agent', 'queued message');
454
+ assert(result.success === true, 'should succeed writing to stopped agent inbox');
455
+ assert(registry.inboxHasMessages('stopped-agent') === true, 'should have messages');
456
+ });
457
+
458
+ // ============================================================
459
+ // Fix 2: --agent value validation (#231)
460
+ // ============================================================
461
+
462
+ runTest('args-parser: --agent rejects flag-like value in general parser', () => {
463
+ // In the general flag parser, --agent followed by a flag-like value
464
+ // does not set args.agent because of the !next.startsWith('-') guard.
465
+ // The chat-specific parser calls process.exit(1) which we can't test here.
466
+ // Test the general parser's behavior instead:
467
+ const result = parseArgs(['search', '--agent', '--verbose', 'keyword']);
468
+ assert(result.agent === null, 'agent should be null when followed by a flag: ' + result.agent);
469
+ });
470
+
471
+ runTest('args-parser: --agent accepts valid name', () => {
472
+ const result = parseArgs(['chat', '--agent', 'my-agent', 'hello']);
473
+ assert(result.agent === 'my-agent', 'agent should be my-agent');
474
+ });
475
+
476
+ runTest('args-parser: chat --agent with flag-like value exits with code 1', () => {
477
+ const cp = require('child_process');
478
+ const tmpDir = path.join(os.tmpdir(), 'pave-test-agent-exit-' + process.pid + '-' + Date.now());
479
+ fs.mkdirSync(tmpDir, { recursive: true });
480
+ const result = cp.spawnSync(
481
+ process.execPath,
482
+ [path.join(__dirname, '..', 'index.js'), 'chat', '--agent', '--no-stream'],
483
+ { timeout: 5000, encoding: 'utf8', cwd: tmpDir, env: { ...process.env, HOME: tmpDir, NO_COLOR: '1' } },
484
+ );
485
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
486
+ assert(result.status === 1, 'should exit with code 1, got: ' + result.status);
487
+ });
488
+
489
+ runTest('args-parser: chat --agent with missing value exits with code 1', () => {
490
+ const cp = require('child_process');
491
+ const tmpDir = path.join(os.tmpdir(), 'pave-test-agent-exit-' + process.pid + '-' + Date.now());
492
+ fs.mkdirSync(tmpDir, { recursive: true });
493
+ const result = cp.spawnSync(
494
+ process.execPath,
495
+ [path.join(__dirname, '..', 'index.js'), 'chat', '--agent'],
496
+ { timeout: 5000, encoding: 'utf8', cwd: tmpDir, env: { ...process.env, HOME: tmpDir, NO_COLOR: '1' } },
497
+ );
498
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
499
+ assert(result.status === 1, 'should exit with code 1, got: ' + result.status);
500
+ });
501
+
502
+ // ============================================================
503
+ // Fix 3: drainInbox deferred deletion (#231)
504
+ // ============================================================
505
+
506
+ resetTestDir();
507
+
508
+ runTest('drainInbox: deleteFiles=false preserves files on disk', () => {
509
+ resetTestDir();
510
+ registry.writeInboxMessage('defer-agent', 'msg1');
511
+ registry.writeInboxMessage('defer-agent', 'msg2');
512
+
513
+ const result = registry.drainInbox('defer-agent', { deleteFiles: false });
514
+ assert(result.messages.length === 2, 'should read 2 messages');
515
+ assert(Array.isArray(result.files), 'should return files array');
516
+ assert(result.files.length === 2, 'should have 2 file paths');
517
+
518
+ // Files should still exist
519
+ assert(registry.inboxHasMessages('defer-agent'), 'inbox should still have messages');
520
+ });
521
+
522
+ runTest('drainInbox: default behavior still deletes files', () => {
523
+ resetTestDir();
524
+ registry.writeInboxMessage('default-agent', 'msg1');
525
+
526
+ const result = registry.drainInbox('default-agent');
527
+ assert(result.messages.length === 1, 'should read 1 message');
528
+ assert(!registry.inboxHasMessages('default-agent'), 'inbox should be empty after default drain');
529
+ });
530
+
531
+ runTest('deleteInboxFiles: removes specified files', () => {
532
+ resetTestDir();
533
+ registry.writeInboxMessage('delete-agent', 'msg1');
534
+ registry.writeInboxMessage('delete-agent', 'msg2');
535
+
536
+ const result = registry.drainInbox('delete-agent', { deleteFiles: false });
537
+ assert(registry.inboxHasMessages('delete-agent'), 'files should exist before delete');
538
+
539
+ registry.deleteInboxFiles(result.files);
540
+ assert(!registry.inboxHasMessages('delete-agent'), 'files should be gone after deleteInboxFiles');
541
+ });
542
+
543
+ runTest('deleteInboxFiles: handles null/undefined gracefully', () => {
544
+ // Should not throw
545
+ registry.deleteInboxFiles(null);
546
+ registry.deleteInboxFiles(undefined);
547
+ registry.deleteInboxFiles([]);
548
+ });
549
+
550
+ runTest('drainInbox: files array contains absolute paths', () => {
551
+ resetTestDir();
552
+ registry.writeInboxMessage('path-agent', 'msg1');
553
+
554
+ const result = registry.drainInbox('path-agent', { deleteFiles: false });
555
+ assert(result.files.length === 1, 'should have 1 file');
556
+ assert(path.isAbsolute(result.files[0]), 'file path should be absolute: ' + result.files[0]);
557
+
558
+ // Cleanup
559
+ registry.deleteInboxFiles(result.files);
560
+ });
561
+
562
+ // ============================================================
563
+ // Cleanup
564
+ // ============================================================
565
+
566
+ cleanup();
567
+
568
+ // ============================================================
569
+ // Summary
570
+ // ============================================================
571
+
572
+ console.log('');
573
+ console.log('Total: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
574
+ if (failed > 0) {
575
+ process.exitCode = 1;
576
+ }