@cnrai/pave 0.3.35 → 0.3.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -218
- package/package.json +32 -35
- package/pave.js +3 -0
- package/sandbox/SandboxRunner.js +1 -0
- package/sandbox/pave-run.js +2 -0
- package/sandbox/permission.js +1 -0
- package/sandbox/utils/yaml.js +1 -0
- package/MARKETPLACE.md +0 -406
- package/build-binary.js +0 -591
- package/build-npm.js +0 -537
- package/build.js +0 -230
- package/check-binary.js +0 -26
- package/deploy.sh +0 -95
- package/index.js +0 -5776
- package/lib/agent-registry.js +0 -1037
- package/lib/args-parser.js +0 -837
- package/lib/blessed-widget-patched.js +0 -93
- package/lib/cli-markdown.js +0 -590
- package/lib/compaction.js +0 -153
- package/lib/duration.js +0 -94
- package/lib/hash.js +0 -22
- package/lib/marketplace.js +0 -866
- package/lib/memory-config.js +0 -166
- package/lib/skill-manager.js +0 -891
- package/lib/soul.js +0 -31
- package/lib/tool-output-formatter.js +0 -180
- package/start-pave.sh +0 -149
- package/status.js +0 -271
- package/test/abort-stream.test.js +0 -445
- package/test/agent-auto-compaction.test.js +0 -552
- package/test/agent-comm-abort.test.js +0 -95
- package/test/agent-comm.test.js +0 -598
- package/test/agent-inbox.test.js +0 -576
- package/test/agent-init.test.js +0 -264
- package/test/agent-interrupt.test.js +0 -314
- package/test/agent-lifecycle.test.js +0 -520
- package/test/agent-log-files.test.js +0 -349
- package/test/agent-mode.manual-test.js +0 -392
- package/test/agent-parsing.test.js +0 -228
- package/test/agent-post-stream-idle.test.js +0 -762
- package/test/agent-registry.test.js +0 -359
- package/test/agent-rm.test.js +0 -442
- package/test/agent-spawn.test.js +0 -933
- package/test/agent-status-api.test.js +0 -624
- package/test/agent-update.test.js +0 -435
- package/test/args-parser.test.js +0 -391
- package/test/auto-compaction-chat.manual-test.js +0 -227
- package/test/auto-compaction.test.js +0 -941
- package/test/build-config.test.js +0 -120
- package/test/build-npm.test.js +0 -388
- package/test/chat-command.test.js +0 -137
- package/test/chat-leading-lines.test.js +0 -159
- package/test/config-flag.test.js +0 -272
- package/test/cursor-drift.test.js +0 -135
- package/test/debug-require.js +0 -23
- package/test/dir-migration.test.js +0 -323
- package/test/duration.test.js +0 -229
- package/test/ghostty-term.test.js +0 -202
- package/test/http500-backoff.test.js +0 -854
- package/test/integration.test.js +0 -86
- package/test/memory-guard-env.test.js +0 -220
- package/test/pr233-fixes.test.js +0 -259
- package/test/run-agent-init.js +0 -297
- package/test/run-all.js +0 -64
- package/test/run-config-flag.js +0 -159
- package/test/run-cursor-drift.js +0 -82
- package/test/run-session-path.js +0 -154
- package/test/run-tests.js +0 -643
- package/test/sandbox-redirect.test.js +0 -202
- package/test/session-path.test.js +0 -132
- package/test/shebang-strip.test.js +0 -241
- package/test/soul-reinject.test.js +0 -1027
- package/test/soul-reread.test.js +0 -281
- package/test/tool-output-formatter.test.js +0 -486
- package/test/tool-output-gating.test.js +0 -143
- package/test/tool-states.test.js +0 -167
- package/test/tools-flag.test.js +0 -65
- package/test/tui-attach.test.js +0 -1255
- package/test/tui-compaction.test.js +0 -354
- package/test/tui-wrap.test.js +0 -568
- package/test-binary.js +0 -52
- package/test-binary2.js +0 -36
package/test/agent-init.test.js
DELETED
|
@@ -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;
|