@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,264 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Standalone test runner for 'pave agent init'
4
- * Designed to work in Node 16 sandbox (no private class fields)
5
- *
6
- * Always runs args-parser tests for 'agent init'. When the Permission
7
- * module can be imported, also runs Permission-related tests; full
8
- * Permission system coverage is exercised in CI where Node supports # fields.
9
- *
10
- * Uses checkmark/X output format (same as other .test.js files) so run-all.js
11
- * correctly counts individual test results. Also throws on failure so
12
- * run-tests.js (require-based runner) reliably catches regressions.
13
- */
14
-
15
- let passCount = 0;
16
- let failCount = 0;
17
- let skipCount = 0;
18
-
19
- function runTest(name, testFn) {
20
- try {
21
- testFn();
22
- console.log('\u2705 ' + name);
23
- passCount++;
24
- } catch (error) {
25
- if (error.message === 'SKIP') {
26
- console.log('SKIP: ' + name);
27
- skipCount++;
28
- } else {
29
- console.log('\u274C ' + name + ': ' + error.message);
30
- failCount++;
31
- process.exitCode = 1;
32
- }
33
- }
34
- }
35
-
36
- function assertEqual(actual, expected, message) {
37
- if (actual !== expected) {
38
- throw new Error(message + ': expected ' + JSON.stringify(expected) + ', got ' + JSON.stringify(actual));
39
- }
40
- }
41
-
42
- // Import parseArgs via absolute path to bypass any test runner mocks
43
- const parseArgs = require(require('path').resolve(__dirname, '..', 'lib', 'args-parser.js')).parseArgs;
44
-
45
- // =============================================
46
- // Args-parser tests for 'agent init'
47
- // =============================================
48
-
49
- runTest('agent init sets agentSubcommand to init', () => {
50
- const args = parseArgs(['agent', 'init']);
51
- assertEqual(args.command, 'agent', 'command');
52
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand');
53
- });
54
-
55
- runTest('agent init does not set SOUL defaults', () => {
56
- const args = parseArgs(['agent', 'init']);
57
- assertEqual(args.sleep, null, 'sleep should be null');
58
- assertEqual(args.commandArgs.length, 0, 'commandArgs should be empty');
59
- });
60
-
61
- runTest('agent init still applies config default', () => {
62
- const args = parseArgs(['agent', 'init']);
63
- assertEqual(args.config, './.pave', 'config should default to ./.pave');
64
- });
65
-
66
- runTest('agent init with --config uses custom path', () => {
67
- const args = parseArgs(['agent', 'init', '--config', '/tmp/my-pave']);
68
- assertEqual(args.command, 'agent', 'command');
69
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand');
70
- assertEqual(args.config, '/tmp/my-pave', 'config');
71
- });
72
-
73
- runTest('agent init does not treat init as a SOUL path', () => {
74
- const args = parseArgs(['agent', 'init']);
75
- assertEqual(args.commandArgs.length, 0, 'init should not be in commandArgs');
76
- });
77
-
78
- runTest('agent without init does not set agentSubcommand', () => {
79
- const args = parseArgs(['agent']);
80
- assertEqual(args.agentSubcommand, null, 'agentSubcommand should be null');
81
- assertEqual(args.command, 'agent', 'command');
82
- // Normal agent should still get defaults
83
- assertEqual(args.sleep, '1m', 'sleep should default to 1m');
84
- assertEqual(args.commandArgs[0], 'AGENTS.md', 'SOUL should default to AGENTS.md');
85
- });
86
-
87
- runTest('agent with SOUL file does not set agentSubcommand', () => {
88
- const args = parseArgs(['agent', 'SOUL.md']);
89
- assertEqual(args.agentSubcommand, null, 'agentSubcommand should be null');
90
- assertEqual(args.commandArgs[0], 'SOUL.md', 'SOUL.md should be in commandArgs');
91
- });
92
-
93
- runTest('agent init --help sets help flag', () => {
94
- const args = parseArgs(['agent', 'init', '--help']);
95
- assertEqual(args.command, 'agent', 'command');
96
- assertEqual(args.help, true, 'help should be true');
97
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand should still be init when using --help');
98
- // When --help is set, SOUL-related defaults are not applied, but agentSubcommand
99
- // detection has already run during the initial positional-argument scan.
100
- });
101
-
102
- runTest('agent init with --config before init works', () => {
103
- // '--config path init' - config flag with value then init; parser skips the
104
- // config value so 'init' is still detected as the agentSubcommand.
105
- const args = parseArgs(['agent', '--config', '/tmp/test', 'init']);
106
- assertEqual(args.command, 'agent', 'command');
107
- assertEqual(args.config, '/tmp/test', 'config');
108
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand should be init');
109
- assertEqual(args.commandArgs.length, 0, 'init should not appear in commandArgs');
110
- });
111
-
112
- runTest('agent init with --verbose passes through', () => {
113
- const args = parseArgs(['agent', 'init', '--verbose']);
114
- assertEqual(args.command, 'agent', 'command');
115
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand');
116
- assertEqual(args.verbose, true, 'verbose');
117
- });
118
-
119
- runTest('agent init with --debug passes through', () => {
120
- const args = parseArgs(['agent', 'init', '--debug']);
121
- assertEqual(args.command, 'agent', 'command');
122
- assertEqual(args.agentSubcommand, 'init', 'agentSubcommand');
123
- assertEqual(args.debug, true, 'debug');
124
- });
125
-
126
- runTest('normal agent command with --config still gets defaults', () => {
127
- const args = parseArgs(['agent', '--config', '/tmp/test']);
128
- assertEqual(args.command, 'agent', 'command');
129
- assertEqual(args.agentSubcommand, null, 'agentSubcommand should be null');
130
- assertEqual(args.config, '/tmp/test', 'config');
131
- assertEqual(args.sleep, '1m', 'sleep should default to 1m');
132
- assertEqual(args.commandArgs[0], 'AGENTS.md', 'SOUL should default to AGENTS.md');
133
- });
134
-
135
- runTest('agent --help does not set agentSubcommand', () => {
136
- const args = parseArgs(['agent', '--help']);
137
- assertEqual(args.command, 'agent', 'command');
138
- assertEqual(args.help, true, 'help');
139
- // Help skips defaults, so agentSubcommand stays null
140
- assertEqual(args.agentSubcommand, null, 'agentSubcommand should be null');
141
- });
142
-
143
- // =============================================
144
- // Permission system tests (skip in sandbox)
145
- // =============================================
146
-
147
- runTest('Permission.setConfigDir updates paths', () => {
148
- let Permission;
149
- try {
150
- Permission = require('../../opencode-lite/src/tools/permission');
151
- } catch (e) {
152
- if (e instanceof SyntaxError || (e && typeof e.message === 'string' && /private field|class fields|Unexpected token #|not followed by identifier/i.test(e.message))) {
153
- throw new Error('SKIP');
154
- }
155
- throw e;
156
- }
157
- const os = require('os');
158
- const path = require('path');
159
- const testDir = path.join(os.tmpdir(), 'pave-test-config-' + Date.now());
160
-
161
- try {
162
- Permission.setConfigDir(testDir);
163
- const paths = Permission.getConfigPaths();
164
- assertEqual(paths.paveDir, testDir, 'paveDir');
165
- assertEqual(paths.permissionsFile, path.join(testDir, 'permissions.yaml'), 'permissionsFile');
166
- assertEqual(Permission.getConfigDir(), testDir, 'getConfigDir');
167
- } finally {
168
- Permission.setConfigDir(path.join(os.homedir(), '.pave'));
169
- }
170
- });
171
-
172
- runTest('Permission.createDefaultPermissions creates file', () => {
173
- let Permission;
174
- try {
175
- Permission = require('../../opencode-lite/src/tools/permission');
176
- } catch (e) {
177
- if (e instanceof SyntaxError || (e && typeof e.message === 'string' && /private field|class fields|Unexpected token #|not followed by identifier/i.test(e.message))) {
178
- throw new Error('SKIP');
179
- }
180
- throw e;
181
- }
182
- const fs = require('fs');
183
- const os = require('os');
184
- const path = require('path');
185
- const testDir = path.join(os.tmpdir(), 'pave-test-create-' + Date.now());
186
-
187
- try {
188
- Permission.setConfigDir(testDir);
189
- const result = Permission.createDefaultPermissions();
190
- assertEqual(result, true, 'should return true');
191
-
192
- const permFile = path.join(testDir, 'permissions.yaml');
193
- assertEqual(fs.existsSync(permFile), true, 'file should exist');
194
-
195
- const content = fs.readFileSync(permFile, 'utf8');
196
- assertEqual(content.indexOf('version: 4') !== -1, true, 'should contain version 4');
197
- assertEqual(content.indexOf("- '*'") !== -1, true, 'should contain wildcard');
198
-
199
- // Verify in-memory permissions reflect the newly created defaults
200
- // createDefaultPermissions() now calls loadPermissions(true) internally,
201
- // so permission checks should pass immediately without a separate reload
202
- assertEqual(
203
- Permission.checkPermission('network', 'example.com'),
204
- true,
205
- 'network permission should be granted by default permissions',
206
- );
207
- } finally {
208
- // Cleanup
209
- try { fs.unlinkSync(path.join(testDir, 'permissions.yaml')); } catch (e) {}
210
- try { fs.rmdirSync(testDir); } catch (e) {}
211
- Permission.setConfigDir(path.join(os.homedir(), '.pave'));
212
- }
213
- });
214
-
215
- runTest('Permission.createDefaultPermissions does not overwrite', () => {
216
- let Permission;
217
- try {
218
- Permission = require('../../opencode-lite/src/tools/permission');
219
- } catch (e) {
220
- if (e instanceof SyntaxError || (e && typeof e.message === 'string' && /private field|class fields|Unexpected token #|not followed by identifier/i.test(e.message))) {
221
- throw new Error('SKIP');
222
- }
223
- throw e;
224
- }
225
- const fs = require('fs');
226
- const os = require('os');
227
- const path = require('path');
228
- const testDir = path.join(os.tmpdir(), 'pave-test-nooverwrite-' + Date.now());
229
- fs.mkdirSync(testDir, { recursive: true });
230
-
231
- const permFile = path.join(testDir, 'permissions.yaml');
232
- try {
233
- fs.writeFileSync(permFile, 'custom: content\n');
234
-
235
- Permission.setConfigDir(testDir);
236
- const result = Permission.createDefaultPermissions();
237
- assertEqual(result, false, 'should return false');
238
-
239
- const content = fs.readFileSync(permFile, 'utf8');
240
- assertEqual(content, 'custom: content\n', 'file should not be overwritten');
241
- } finally {
242
- // Cleanup
243
- try { fs.unlinkSync(permFile); } catch (e) {}
244
- try { fs.rmdirSync(testDir); } catch (e) {}
245
- Permission.setConfigDir(path.join(os.homedir(), '.pave'));
246
- }
247
- });
248
-
249
- // =============================================
250
- // Summary
251
- // =============================================
252
-
253
- console.log('\n--- Agent Init Test Summary ---');
254
- console.log('Passed: ' + passCount);
255
- console.log('Failed: ' + failCount);
256
- console.log('Skipped: ' + skipCount);
257
- console.log('Total: ' + (passCount + failCount + skipCount));
258
-
259
- if (failCount > 0) {
260
- console.log('\nSome tests FAILED.');
261
- throw new Error('Some tests FAILED.');
262
- } else {
263
- console.log('\nAll runnable tests passed!');
264
- }
@@ -1,314 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Tests for agent interrupt protocol (Issue #201)
4
- * Phase 4: Urgent interrupt via --urgent flag + sleep wake-up
5
- */
6
-
7
- const path = require('path');
8
- const fs = require('fs');
9
- const os = require('os');
10
-
11
- // Determine registry path
12
- const registryPath = path.join(__dirname, '..', 'lib', 'agent-registry.js');
13
- const parserPath = path.join(__dirname, '..', 'lib', 'args-parser.js');
14
- const registry = require(registryPath);
15
- const parseArgs = require(parserPath).parseArgs;
16
-
17
- // Use a unique test directory to avoid interfering with real agents
18
- const testDir = path.join(os.tmpdir(), 'pave-test-interrupt-' + process.pid);
19
-
20
- let passed = 0;
21
- let failed = 0;
22
-
23
- function assert(condition, msg) {
24
- if (!condition) throw new Error('Assertion failed: ' + msg);
25
- }
26
-
27
- function runTest(name, fn) {
28
- try {
29
- fn();
30
- console.log('[PASS] ' + name);
31
- passed++;
32
- } catch (e) {
33
- console.log('[FAIL] ' + name + ': ' + e.message);
34
- failed++;
35
- }
36
- }
37
-
38
- function resetTestDir() {
39
- try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
40
- registry.setAgentsDir(testDir);
41
- }
42
-
43
- function cleanup() {
44
- try { fs.rmSync(testDir, { recursive: true }); } catch (e) {}
45
- registry.resetAgentsDir();
46
- }
47
-
48
- // ============================================================
49
- // Registry: writeInterrupt / readInterrupt / hasInterrupt / clearInterrupt
50
- // ============================================================
51
-
52
- resetTestDir();
53
-
54
- runTest('writeInterrupt: returns success for valid message', () => {
55
- resetTestDir();
56
- const result = registry.writeInterrupt('test-agent', 'stop everything');
57
- assert(result.success === true, 'should succeed');
58
- assert(result.error === null, 'should have no error');
59
- });
60
-
61
- runTest('writeInterrupt: creates interrupt.json file', () => {
62
- resetTestDir();
63
- registry.writeInterrupt('test-agent', 'urgent message');
64
- const interruptPath = registry.getInterruptPath('test-agent');
65
- assert(fs.existsSync(interruptPath), 'interrupt.json should exist');
66
- const content = JSON.parse(fs.readFileSync(interruptPath, 'utf8'));
67
- assert(content.message === 'urgent message', 'message should match');
68
- assert(content.action === 'abort', 'default action should be abort');
69
- assert(content.from === 'tui', 'default from should be tui');
70
- assert(typeof content.timestamp === 'number', 'timestamp should be a number');
71
- });
72
-
73
- runTest('writeInterrupt: rejects empty message', () => {
74
- resetTestDir();
75
- const result = registry.writeInterrupt('test-agent', '');
76
- assert(result.success === false, 'should fail');
77
- assert(result.error.indexOf('non-empty') !== -1, 'error should mention non-empty');
78
- });
79
-
80
- runTest('writeInterrupt: rejects null message', () => {
81
- resetTestDir();
82
- const result = registry.writeInterrupt('test-agent', null);
83
- assert(result.success === false, 'should fail');
84
- });
85
-
86
- runTest('writeInterrupt: accepts custom from and action', () => {
87
- resetTestDir();
88
- registry.writeInterrupt('test-agent', 'msg', { from: 'api', action: 'abort' });
89
- const content = JSON.parse(fs.readFileSync(registry.getInterruptPath('test-agent'), 'utf8'));
90
- assert(content.from === 'api', 'from should be api');
91
- assert(content.action === 'abort', 'action should be abort');
92
- });
93
-
94
- runTest('writeInterrupt: latest write wins (overwrite)', () => {
95
- resetTestDir();
96
- registry.writeInterrupt('test-agent', 'first message');
97
- registry.writeInterrupt('test-agent', 'second message');
98
- const content = JSON.parse(fs.readFileSync(registry.getInterruptPath('test-agent'), 'utf8'));
99
- assert(content.message === 'second message', 'should be overwritten with latest');
100
- });
101
-
102
- runTest('hasInterrupt: returns false when no interrupt', () => {
103
- resetTestDir();
104
- assert(registry.hasInterrupt('nonexistent') === false, 'should return false');
105
- });
106
-
107
- runTest('hasInterrupt: returns true when interrupt exists', () => {
108
- resetTestDir();
109
- registry.writeInterrupt('test-agent', 'msg');
110
- assert(registry.hasInterrupt('test-agent') === true, 'should return true');
111
- });
112
-
113
- runTest('readInterrupt: returns envelope when interrupt exists', () => {
114
- resetTestDir();
115
- registry.writeInterrupt('test-agent', 'check production');
116
- const result = registry.readInterrupt('test-agent');
117
- assert(result !== null, 'should not be null');
118
- assert(result.message === 'check production', 'message should match');
119
- assert(result.action === 'abort', 'action should be abort');
120
- assert(result.from === 'tui', 'from should be tui');
121
- });
122
-
123
- runTest('readInterrupt: returns null when no interrupt', () => {
124
- resetTestDir();
125
- const result = registry.readInterrupt('nonexistent');
126
- assert(result === null, 'should return null');
127
- });
128
-
129
- runTest('readInterrupt: returns null for malformed JSON', () => {
130
- resetTestDir();
131
- const dir = registry.getAgentDir('test-agent');
132
- fs.mkdirSync(dir, { recursive: true });
133
- fs.writeFileSync(path.join(dir, 'interrupt.json'), 'not json');
134
- const result = registry.readInterrupt('test-agent');
135
- assert(result === null, 'should return null for malformed JSON');
136
- });
137
-
138
- runTest('readInterrupt: returns null for missing message field', () => {
139
- resetTestDir();
140
- const dir = registry.getAgentDir('test-agent');
141
- fs.mkdirSync(dir, { recursive: true });
142
- fs.writeFileSync(path.join(dir, 'interrupt.json'), JSON.stringify({ action: 'abort' }));
143
- const result = registry.readInterrupt('test-agent');
144
- assert(result === null, 'should return null when message field missing');
145
- });
146
-
147
- runTest('clearInterrupt: removes the interrupt file', () => {
148
- resetTestDir();
149
- registry.writeInterrupt('test-agent', 'msg');
150
- assert(registry.hasInterrupt('test-agent') === true, 'should exist before clear');
151
- registry.clearInterrupt('test-agent');
152
- assert(registry.hasInterrupt('test-agent') === false, 'should not exist after clear');
153
- });
154
-
155
- runTest('clearInterrupt: no error when no interrupt exists', () => {
156
- resetTestDir();
157
- // Should not throw
158
- registry.clearInterrupt('nonexistent');
159
- });
160
-
161
- runTest('getInterruptPath: returns expected path', () => {
162
- resetTestDir();
163
- const p = registry.getInterruptPath('my-agent');
164
- assert(p.indexOf('my-agent') !== -1, 'should contain agent name');
165
- assert(p.endsWith('interrupt.json'), 'should end with interrupt.json');
166
- });
167
-
168
- // ============================================================
169
- // Args parser: --urgent flag
170
- // ============================================================
171
-
172
- runTest('args-parser: --urgent sets urgent=true', () => {
173
- const result = parseArgs(['chat', '-a', 'my-agent', '--urgent', 'stop everything']);
174
- assert(result.urgent === true, 'urgent should be true');
175
- assert(result.agent === 'my-agent', 'agent should be set');
176
- });
177
-
178
- runTest('args-parser: urgent defaults to false', () => {
179
- const result = parseArgs(['chat', '-a', 'my-agent', 'hello']);
180
- assert(result.urgent === false, 'urgent should default to false');
181
- });
182
-
183
- runTest('args-parser: --urgent works with --agent long form', () => {
184
- const result = parseArgs(['chat', '--agent', 'test', '--urgent', 'message']);
185
- assert(result.urgent === true, 'urgent should be true');
186
- assert(result.agent === 'test', 'agent should be test');
187
- });
188
-
189
- runTest('args-parser: --urgent without -a still parses', () => {
190
- const result = parseArgs(['chat', '--urgent', 'message']);
191
- assert(result.urgent === true, 'urgent should be true');
192
- assert(result.agent === null, 'agent should be null');
193
- });
194
-
195
- // ============================================================
196
- // Integration: interrupt + inbox interaction
197
- // ============================================================
198
-
199
- runTest('interrupt does not affect inbox', () => {
200
- resetTestDir();
201
- // Write both interrupt and inbox message
202
- registry.writeInterrupt('test-agent', 'urgent msg');
203
- registry.writeInboxMessage('test-agent', 'normal msg');
204
-
205
- // Both should exist
206
- assert(registry.hasInterrupt('test-agent') === true, 'interrupt should exist');
207
- assert(registry.inboxHasMessages('test-agent') === true, 'inbox should have messages');
208
-
209
- // Clear interrupt shouldn't affect inbox
210
- registry.clearInterrupt('test-agent');
211
- assert(registry.hasInterrupt('test-agent') === false, 'interrupt should be cleared');
212
- assert(registry.inboxHasMessages('test-agent') === true, 'inbox should still have messages');
213
- });
214
-
215
- runTest('multiple interrupts: only last survives', () => {
216
- resetTestDir();
217
- registry.writeInterrupt('test-agent', 'first');
218
- registry.writeInterrupt('test-agent', 'second');
219
- registry.writeInterrupt('test-agent', 'third');
220
-
221
- const result = registry.readInterrupt('test-agent');
222
- assert(result.message === 'third', 'should be the last write');
223
- });
224
-
225
- // ============================================================
226
- // signalAwareSleep: verify polling behavior via source inspection
227
- // ============================================================
228
-
229
- runTest('signalAwareSleep uses setInterval not setTimeout', () => {
230
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
231
- // Find signalAwareSleep function
232
- const funcStart = source.indexOf('function signalAwareSleep');
233
- assert(funcStart !== -1, 'should find signalAwareSleep');
234
- const funcEnd = source.indexOf('\n}', funcStart + 100);
235
- const funcBody = source.substring(funcStart, funcEnd + 2);
236
-
237
- assert(funcBody.indexOf('setInterval') !== -1, 'should use setInterval for polling');
238
- assert(funcBody.indexOf('POLL_INTERVAL') !== -1, 'should define POLL_INTERVAL');
239
- // Should NOT use setTimeout as main timer
240
- // (clearInterval is acceptable)
241
- assert(funcBody.indexOf('setTimeout') === -1, 'should not use setTimeout');
242
- });
243
-
244
- runTest('sleep calls include interrupt check in condition', () => {
245
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
246
- // Count lines that have both signalAwareSleep and hasInterrupt
247
- const lines = source.split('\n');
248
- let count = 0;
249
- for (let i = 0; i < lines.length; i++) {
250
- if (lines[i].indexOf('signalAwareSleep') !== -1 && lines[i].indexOf('hasInterrupt') !== -1) {
251
- count++;
252
- }
253
- }
254
- assert(count >= 2, 'at least 2 sleep calls should check hasInterrupt, found ' + count);
255
- });
256
-
257
- // ============================================================
258
- // Agent loop: interrupt check placement
259
- // ============================================================
260
-
261
- runTest('agent loop checks interrupt before inbox/SOUL', () => {
262
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
263
- const interruptCheck = source.indexOf('hasInterrupt(agentName)');
264
- const inboxCheck = source.indexOf('inboxHasMessages(agentName)');
265
- assert(interruptCheck !== -1, 'should check hasInterrupt in agent loop');
266
- assert(inboxCheck !== -1, 'should check inboxHasMessages in agent loop');
267
- assert(interruptCheck < inboxCheck, 'interrupt check should come before inbox check');
268
- });
269
-
270
- runTest('agent loop formats interrupt with URGENT prefix', () => {
271
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
272
- assert(source.indexOf('URGENT INTERRUPT') !== -1, 'should format interrupt with URGENT prefix');
273
- });
274
-
275
- runTest('agent loop clears interrupt after reading', () => {
276
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
277
- assert(source.indexOf('clearInterrupt(agentName)') !== -1, 'should call clearInterrupt');
278
- });
279
-
280
- // ============================================================
281
- // handleChatToAgent: --urgent routing
282
- // ============================================================
283
-
284
- runTest('handleChatToAgent routes --urgent to writeInterrupt (source check)', () => {
285
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
286
- const funcStart = source.indexOf('async function handleChatToAgent');
287
- assert(funcStart !== -1, 'should find handleChatToAgent');
288
- let funcEnd = source.indexOf('\nasync function', funcStart + 50);
289
- if (funcEnd === -1) funcEnd = source.length;
290
- const funcBody = source.substring(funcStart, funcEnd);
291
-
292
- assert(funcBody.indexOf('writeInterrupt') !== -1, 'should call writeInterrupt for urgent');
293
- assert(funcBody.indexOf('writeInboxMessage') !== -1, 'should still call writeInboxMessage for non-urgent');
294
- });
295
-
296
- runTest('handleChatToAgent errors on --urgent without --agent (source check)', () => {
297
- const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
298
- const funcStart = source.indexOf('async function handleChatToAgent');
299
- let funcEnd = source.indexOf('\nasync function', funcStart + 50);
300
- if (funcEnd === -1) funcEnd = source.length;
301
- const funcBody = source.substring(funcStart, funcEnd);
302
-
303
- assert(funcBody.indexOf('Cannot send urgent interrupt without specifying an agent') !== -1,
304
- 'should have error message for --urgent without --agent');
305
- });
306
-
307
- // ============================================================
308
- // Cleanup & Summary
309
- // ============================================================
310
-
311
- cleanup();
312
-
313
- console.log('\nTotal: ' + (passed + failed) + ', Passed: ' + passed + ', Failed: ' + failed);
314
- if (failed > 0) process.exitCode = 1;