@cnrai/pave 0.3.33 → 0.3.35
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/MARKETPLACE.md +406 -0
- package/README.md +218 -21
- package/build-binary.js +591 -0
- package/build-npm.js +537 -0
- package/build.js +230 -0
- package/check-binary.js +26 -0
- package/deploy.sh +95 -0
- package/index.js +5776 -0
- package/lib/agent-registry.js +1037 -0
- package/lib/args-parser.js +837 -0
- package/lib/blessed-widget-patched.js +93 -0
- package/lib/cli-markdown.js +590 -0
- package/lib/compaction.js +153 -0
- package/lib/duration.js +94 -0
- package/lib/hash.js +22 -0
- package/lib/marketplace.js +866 -0
- package/lib/memory-config.js +166 -0
- package/lib/skill-manager.js +891 -0
- package/lib/soul.js +31 -0
- package/lib/tool-output-formatter.js +180 -0
- package/package.json +35 -32
- package/start-pave.sh +149 -0
- package/status.js +271 -0
- package/test/abort-stream.test.js +445 -0
- package/test/agent-auto-compaction.test.js +552 -0
- package/test/agent-comm-abort.test.js +95 -0
- package/test/agent-comm.test.js +598 -0
- package/test/agent-inbox.test.js +576 -0
- package/test/agent-init.test.js +264 -0
- package/test/agent-interrupt.test.js +314 -0
- package/test/agent-lifecycle.test.js +520 -0
- package/test/agent-log-files.test.js +349 -0
- package/test/agent-mode.manual-test.js +392 -0
- package/test/agent-parsing.test.js +228 -0
- package/test/agent-post-stream-idle.test.js +762 -0
- package/test/agent-registry.test.js +359 -0
- package/test/agent-rm.test.js +442 -0
- package/test/agent-spawn.test.js +933 -0
- package/test/agent-status-api.test.js +624 -0
- package/test/agent-update.test.js +435 -0
- package/test/args-parser.test.js +391 -0
- package/test/auto-compaction-chat.manual-test.js +227 -0
- package/test/auto-compaction.test.js +941 -0
- package/test/build-config.test.js +120 -0
- package/test/build-npm.test.js +388 -0
- package/test/chat-command.test.js +137 -0
- package/test/chat-leading-lines.test.js +159 -0
- package/test/config-flag.test.js +272 -0
- package/test/cursor-drift.test.js +135 -0
- package/test/debug-require.js +23 -0
- package/test/dir-migration.test.js +323 -0
- package/test/duration.test.js +229 -0
- package/test/ghostty-term.test.js +202 -0
- package/test/http500-backoff.test.js +854 -0
- package/test/integration.test.js +86 -0
- package/test/memory-guard-env.test.js +220 -0
- package/test/pr233-fixes.test.js +259 -0
- package/test/run-agent-init.js +297 -0
- package/test/run-all.js +64 -0
- package/test/run-config-flag.js +159 -0
- package/test/run-cursor-drift.js +82 -0
- package/test/run-session-path.js +154 -0
- package/test/run-tests.js +643 -0
- package/test/sandbox-redirect.test.js +202 -0
- package/test/session-path.test.js +132 -0
- package/test/shebang-strip.test.js +241 -0
- package/test/soul-reinject.test.js +1027 -0
- package/test/soul-reread.test.js +281 -0
- package/test/tool-output-formatter.test.js +486 -0
- package/test/tool-output-gating.test.js +143 -0
- package/test/tool-states.test.js +167 -0
- package/test/tools-flag.test.js +65 -0
- package/test/tui-attach.test.js +1255 -0
- package/test/tui-compaction.test.js +354 -0
- package/test/tui-wrap.test.js +568 -0
- package/test-binary.js +52 -0
- package/test-binary2.js +36 -0
- package/LICENSE +0 -21
- package/pave.js +0 -3
- package/sandbox/SandboxRunner.js +0 -1
- package/sandbox/pave-run.js +0 -2
- package/sandbox/permission.js +0 -1
- package/sandbox/utils/yaml.js +0 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Integration test for tool output display functionality
|
|
4
|
+
* Tests the end-to-end behavior of the chat command with tool flags
|
|
5
|
+
* Run with: node test/integration.test.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
function runTest(name, testFn) {
|
|
12
|
+
try {
|
|
13
|
+
testFn();
|
|
14
|
+
console.log(`✅ ${name}`);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.log(`❌ ${name}: ${error.message}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assertEqual(actual, expected, message) {
|
|
22
|
+
if (actual !== expected) {
|
|
23
|
+
throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Test help text includes --no-tools flag
|
|
28
|
+
runTest('Help text should include --no-tools flag', () => {
|
|
29
|
+
const { showHelp } = require('../lib/args-parser');
|
|
30
|
+
|
|
31
|
+
// Capture help output
|
|
32
|
+
let helpOutput = '';
|
|
33
|
+
const originalConsoleLog = console.log;
|
|
34
|
+
console.log = (msg) => { helpOutput += msg + '\n'; };
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
showHelp();
|
|
38
|
+
} finally {
|
|
39
|
+
console.log = originalConsoleLog;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
assertEqual(helpOutput.includes('--no-tools'), true, 'Help should mention --no-tools flag');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Test the main index.js can be loaded without errors
|
|
46
|
+
runTest('Main index.js should load without syntax errors', () => {
|
|
47
|
+
const indexPath = path.join(__dirname, '..', 'index.js');
|
|
48
|
+
assertEqual(fs.existsSync(indexPath), true, 'index.js should exist');
|
|
49
|
+
|
|
50
|
+
// Check that handleChatCommand function includes showTools parameter
|
|
51
|
+
const indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
52
|
+
assertEqual(indexContent.includes('showTools'), true, 'index.js should reference showTools');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Test sendMessageAndStream function signature
|
|
56
|
+
runTest('sendMessageAndStream should accept showTools option', () => {
|
|
57
|
+
const indexPath = path.join(__dirname, '..', 'index.js');
|
|
58
|
+
const indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
59
|
+
|
|
60
|
+
// Check that showTools is passed to sendMessageAndStream
|
|
61
|
+
assertEqual(indexContent.includes('showTools'), true, 'Should reference showTools parameter');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Test args-parser.js has correct defaults (showTools defaults to true)
|
|
65
|
+
runTest('args-parser should have correct default values', () => {
|
|
66
|
+
const argsPath = path.join(__dirname, '..', 'lib', 'args-parser.js');
|
|
67
|
+
const argsContent = fs.readFileSync(argsPath, 'utf8');
|
|
68
|
+
|
|
69
|
+
assertEqual(argsContent.includes('showTools: true'), true, 'Default showTools should be true');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Test flag parsing logic
|
|
73
|
+
runTest('Flag parsing should handle --no-tools in chat command args', () => {
|
|
74
|
+
const argsPath = path.join(__dirname, '..', 'lib', 'args-parser.js');
|
|
75
|
+
const argsContent = fs.readFileSync(argsPath, 'utf8');
|
|
76
|
+
|
|
77
|
+
// Check that --no-tools is recognized
|
|
78
|
+
assertEqual(argsContent.includes('--no-tools'), true, 'Should recognize --no-tools flag');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log('\n🔍 Integration Test Summary:');
|
|
82
|
+
if (process.exitCode === 1) {
|
|
83
|
+
console.log('Some integration tests failed. The changes may not be properly integrated.');
|
|
84
|
+
} else {
|
|
85
|
+
console.log('All integration tests passed! The tool output functionality is properly integrated. 🎉');
|
|
86
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Memory Guard env variable override bug (issue #268)
|
|
3
|
+
*
|
|
4
|
+
* Bug 1: startupLimits are hardcoded — env vars only apply to normalLimits
|
|
5
|
+
* Bug 2: setStartupMode(false) is never called on non-iSH (macOS)
|
|
6
|
+
*
|
|
7
|
+
* Tests verify the fixes via static analysis of the source files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const assert = require('assert');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// Read source files for static analysis
|
|
17
|
+
const guardSource = fs.readFileSync(
|
|
18
|
+
path.join(__dirname, '..', '..', 'opencode-lite', 'src', 'utils', 'memory-guard.js'),
|
|
19
|
+
'utf8',
|
|
20
|
+
);
|
|
21
|
+
const paveSource = fs.readFileSync(
|
|
22
|
+
path.join(__dirname, '..', 'index.js'),
|
|
23
|
+
'utf8',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
let total = 0;
|
|
29
|
+
|
|
30
|
+
function runTest(name, fn) {
|
|
31
|
+
total++;
|
|
32
|
+
try {
|
|
33
|
+
fn();
|
|
34
|
+
console.log('\u2705 ' + name);
|
|
35
|
+
passed++;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log('\u274c ' + name + ': ' + e.message);
|
|
38
|
+
failed++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertContains(src, needle, msg) {
|
|
43
|
+
assert(src.indexOf(needle) !== -1, (msg || '') + ' — missing: "' + needle + '"');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertNotContains(src, needle, msg) {
|
|
47
|
+
assert(src.indexOf(needle) === -1, (msg || '') + ' — found: "' + needle + '"');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================
|
|
51
|
+
// 1. Bug 1: startupLimits respect env overrides
|
|
52
|
+
// =============================================================
|
|
53
|
+
|
|
54
|
+
// Helper: split startupLimits ternary into iSH and non-iSH branches
|
|
55
|
+
function getStartupBranches() {
|
|
56
|
+
const startupIdx = guardSource.indexOf('this.startupLimits');
|
|
57
|
+
assert(startupIdx !== -1, 'should have startupLimits definition');
|
|
58
|
+
const startupBlock = guardSource.substring(startupIdx, startupIdx + 900);
|
|
59
|
+
const parts = startupBlock.split('} : {');
|
|
60
|
+
assert(parts.length >= 2, 'startupLimits should contain a ternary with "} : {"');
|
|
61
|
+
return { ish: parts[0], nonIsh: parts[1], full: startupBlock };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
runTest('startupLimits non-iSH heapUsed uses ENV_HEAP_LIMIT via Math.max', () => {
|
|
65
|
+
const branches = getStartupBranches();
|
|
66
|
+
assertContains(branches.nonIsh, 'Math.max(ENV_HEAP_LIMIT',
|
|
67
|
+
'non-iSH startupLimits heapUsed should use Math.max with ENV_HEAP_LIMIT');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
runTest('startupLimits non-iSH critical uses ENV_CRITICAL_LIMIT via Math.max', () => {
|
|
71
|
+
const branches = getStartupBranches();
|
|
72
|
+
assertContains(branches.nonIsh, 'Math.max(ENV_CRITICAL_LIMIT',
|
|
73
|
+
'non-iSH startupLimits critical should use Math.max with ENV_CRITICAL_LIMIT');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
runTest('startupLimits iSH heapUsed also uses ENV_HEAP_LIMIT via Math.max', () => {
|
|
77
|
+
const branches = getStartupBranches();
|
|
78
|
+
assertContains(branches.ish, 'Math.max(ENV_HEAP_LIMIT',
|
|
79
|
+
'iSH startupLimits should also use ENV_HEAP_LIMIT');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
runTest('startupLimits iSH critical also uses ENV_CRITICAL_LIMIT via Math.max', () => {
|
|
83
|
+
const branches = getStartupBranches();
|
|
84
|
+
assertContains(branches.ish, 'Math.max(ENV_CRITICAL_LIMIT',
|
|
85
|
+
'iSH startupLimits critical should also use ENV_CRITICAL_LIMIT');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
runTest('startupLimits comment references issue #268', () => {
|
|
89
|
+
const startupIdx = guardSource.indexOf('this.startupLimits');
|
|
90
|
+
const commentRegion = guardSource.substring(startupIdx - 200, startupIdx + 50);
|
|
91
|
+
assertContains(commentRegion, '#268',
|
|
92
|
+
'startupLimits fix should reference issue #268');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
runTest('normalLimits still use ENV overrides (unchanged)', () => {
|
|
96
|
+
const normalIdx = guardSource.indexOf('this.normalLimits');
|
|
97
|
+
const normalBlock = guardSource.substring(normalIdx, normalIdx + 600);
|
|
98
|
+
assertContains(normalBlock, 'ENV_HEAP_LIMIT ||',
|
|
99
|
+
'normalLimits should still use ENV_HEAP_LIMIT');
|
|
100
|
+
assertContains(normalBlock, 'ENV_CRITICAL_LIMIT ||',
|
|
101
|
+
'normalLimits should still use ENV_CRITICAL_LIMIT');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// =============================================================
|
|
105
|
+
// 2. Bug 2: setStartupMode(false) called on ALL platforms
|
|
106
|
+
// =============================================================
|
|
107
|
+
|
|
108
|
+
runTest('notifySessionActivated calls setStartupMode(false) without isISH gate', () => {
|
|
109
|
+
const fnIdx = guardSource.indexOf('function notifySessionActivated');
|
|
110
|
+
assert(fnIdx !== -1, 'should have notifySessionActivated function');
|
|
111
|
+
const fnBody = guardSource.substring(fnIdx, fnIdx + 600);
|
|
112
|
+
assertContains(fnBody, 'guard.setStartupMode(false)',
|
|
113
|
+
'should call setStartupMode(false)');
|
|
114
|
+
// Verify no isISH gate: the pattern "if (guard.isISH)" should not appear
|
|
115
|
+
// (guard.isISH may appear in comments explaining the fix, which is fine)
|
|
116
|
+
assertNotContains(fnBody, 'if (guard.isISH)',
|
|
117
|
+
'should NOT gate setStartupMode(false) behind if (guard.isISH)');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
runTest('notifySessionActivated fix references issue #268', () => {
|
|
121
|
+
const fnIdx = guardSource.indexOf('function notifySessionActivated');
|
|
122
|
+
const fnBody = guardSource.substring(fnIdx, fnIdx + 600);
|
|
123
|
+
assertContains(fnBody, '#268',
|
|
124
|
+
'notifySessionActivated fix should reference issue #268');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// =============================================================
|
|
128
|
+
// 3. Bug 2 part 2: index.js setStartupMode(true) on ALL platforms
|
|
129
|
+
// =============================================================
|
|
130
|
+
|
|
131
|
+
runTest('index.js setStartupMode(true) is NOT gated behind isISH', () => {
|
|
132
|
+
const startupIdx = paveSource.indexOf('.setStartupMode(true)');
|
|
133
|
+
assert(startupIdx !== -1, 'should have setStartupMode(true) call');
|
|
134
|
+
// Check the surrounding region — should NOT have 'if (isISH)' guard
|
|
135
|
+
const surroundingBefore = paveSource.substring(startupIdx - 300, startupIdx);
|
|
136
|
+
assertNotContains(surroundingBefore, 'if (isISH)',
|
|
137
|
+
'setStartupMode(true) should NOT be gated behind if (isISH)');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
runTest('index.js setStartupMode block references issue #268', () => {
|
|
141
|
+
const startupIdx = paveSource.indexOf('.setStartupMode(true)');
|
|
142
|
+
const commentRegion = paveSource.substring(startupIdx - 300, startupIdx);
|
|
143
|
+
assertContains(commentRegion, '#268',
|
|
144
|
+
'index.js startup mode fix should reference issue #268');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
runTest('index.js setStartupMode block mentions ALL platforms', () => {
|
|
148
|
+
const startupIdx = paveSource.indexOf('.setStartupMode(true)');
|
|
149
|
+
const commentRegion = paveSource.substring(startupIdx - 400, startupIdx);
|
|
150
|
+
// Should mention that it runs on all platforms now
|
|
151
|
+
assert(
|
|
152
|
+
commentRegion.indexOf('ALL platforms') !== -1 ||
|
|
153
|
+
commentRegion.indexOf('all platforms') !== -1 ||
|
|
154
|
+
commentRegion.indexOf('not just iSH') !== -1,
|
|
155
|
+
'comment should mention running on all platforms',
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// =============================================================
|
|
160
|
+
// 4. Env variable parsing
|
|
161
|
+
// =============================================================
|
|
162
|
+
|
|
163
|
+
runTest('parsePositiveMegabytesEnv exists and handles edge cases', () => {
|
|
164
|
+
assertContains(guardSource, 'function parsePositiveMegabytesEnv',
|
|
165
|
+
'should have env parsing function');
|
|
166
|
+
const fnIdx = guardSource.indexOf('function parsePositiveMegabytesEnv');
|
|
167
|
+
const fnBody = guardSource.substring(fnIdx, fnIdx + 300);
|
|
168
|
+
assertContains(fnBody, 'parseInt', 'should parse integer');
|
|
169
|
+
assertContains(fnBody, 'null', 'should return null for invalid');
|
|
170
|
+
assertContains(fnBody, '1024 * 1024', 'should convert MB to bytes');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
runTest('ENV_HEAP_LIMIT reads from PAVE_MEMORY_HEAP', () => {
|
|
174
|
+
assertContains(guardSource, "parsePositiveMegabytesEnv('PAVE_MEMORY_HEAP')",
|
|
175
|
+
'should read PAVE_MEMORY_HEAP');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
runTest('ENV_CRITICAL_LIMIT reads from PAVE_MEMORY_CRITICAL', () => {
|
|
179
|
+
assertContains(guardSource, "parsePositiveMegabytesEnv('PAVE_MEMORY_CRITICAL')",
|
|
180
|
+
'should read PAVE_MEMORY_CRITICAL');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// =============================================================
|
|
184
|
+
// 5. onFirstSessionActivated still works
|
|
185
|
+
// =============================================================
|
|
186
|
+
|
|
187
|
+
runTest('onFirstSessionActivated callback is still invoked', () => {
|
|
188
|
+
const fnIdx = guardSource.indexOf('function notifySessionActivated');
|
|
189
|
+
const fnBody = guardSource.substring(fnIdx, fnIdx + 800);
|
|
190
|
+
assertContains(fnBody, 'onSessionActivatedCallback',
|
|
191
|
+
'should still call the registered callback');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
runTest('index.js registers onFirstSessionActivated callback', () => {
|
|
195
|
+
assertContains(paveSource, 'onFirstSessionActivated(',
|
|
196
|
+
'should register callback for session activation');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// =============================================================
|
|
200
|
+
// 6. Functional test: MemoryGuard with env overrides
|
|
201
|
+
// =============================================================
|
|
202
|
+
|
|
203
|
+
runTest('MemoryGuard constructor applies env to startupLimits when set', () => {
|
|
204
|
+
// Verify the Math.max pattern ensures env values override hardcoded defaults
|
|
205
|
+
const branches = getStartupBranches();
|
|
206
|
+
// Non-iSH heapUsed: Math.max(ENV_HEAP_LIMIT || 0, 200 * 1024 * 1024)
|
|
207
|
+
// This means if ENV_HEAP_LIMIT=400MB, result = max(400MB, 200MB) = 400MB
|
|
208
|
+
assertContains(branches.nonIsh, 'ENV_HEAP_LIMIT || 0',
|
|
209
|
+
'should use 0 fallback so Math.max picks the hardcoded default when env not set');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// =============================================================
|
|
213
|
+
// Summary
|
|
214
|
+
// =============================================================
|
|
215
|
+
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log('Total: ' + total + ', Passed: ' + passed + ', Failed: ' + failed);
|
|
218
|
+
if (failed > 0) {
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for PR #233 Copilot review fixes (Issue #237)
|
|
7
|
+
*
|
|
8
|
+
* Fix 1: Wrong port fallback in abort HTTP POST (was 19747, should use PAVE_PORT default 4096)
|
|
9
|
+
* Fix 2: Dead EEXIST guard removed from writeInterrupt mkdirSync
|
|
10
|
+
* Fix 3: writeStatus includes port field for agent loop calls
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try {
|
|
22
|
+
fn();
|
|
23
|
+
passed++;
|
|
24
|
+
console.log(`✅ ${name}`);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
failed++;
|
|
27
|
+
console.log(`❌ ${name}: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assert(condition, msg) {
|
|
32
|
+
if (!condition) throw new Error(msg || 'Assertion failed');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertEqual(actual, expected, msg) {
|
|
36
|
+
if (actual !== expected) {
|
|
37
|
+
throw new Error((msg || '') + ` expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Test Suite 1: Fix 1 - Port fallback uses correct default
|
|
43
|
+
// ============================================================
|
|
44
|
+
|
|
45
|
+
console.log('\nTest Suite 1: Fix 1 - Port fallback in abort HTTP POST');
|
|
46
|
+
|
|
47
|
+
test('abort port fallback uses serverUrl not hardcoded port', () => {
|
|
48
|
+
const indexSrc = fs.readFileSync(
|
|
49
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Should NOT contain the old wrong fallback
|
|
53
|
+
assert(
|
|
54
|
+
!indexSrc.includes('status.port || 19747'),
|
|
55
|
+
'index.js still contains wrong port fallback 19747',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Should extract port from serverUrl (the actual listening address)
|
|
59
|
+
assert(
|
|
60
|
+
indexSrc.includes('new URL(status.serverUrl)'),
|
|
61
|
+
'index.js should parse serverUrl to extract real port',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('abort port extraction matches agent-comm.js approach', () => {
|
|
66
|
+
const indexSrc = fs.readFileSync(
|
|
67
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Both pave/index.js and agent-comm.js should parse serverUrl
|
|
71
|
+
// instead of relying on status.port (which is always default 4096)
|
|
72
|
+
assert(
|
|
73
|
+
indexSrc.includes('parsed.port'),
|
|
74
|
+
'Should extract port from parsed URL',
|
|
75
|
+
);
|
|
76
|
+
assert(
|
|
77
|
+
indexSrc.includes('parsed.hostname'),
|
|
78
|
+
'Should extract hostname from parsed URL',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Test Suite 2: Fix 2 - Dead EEXIST guard removed from writeInterrupt
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
console.log('\nTest Suite 2: Fix 2 - Dead EEXIST guard in writeInterrupt');
|
|
87
|
+
|
|
88
|
+
test('writeInterrupt mkdirSync does not have dead EEXIST guard', () => {
|
|
89
|
+
const registrySrc = fs.readFileSync(
|
|
90
|
+
path.join(__dirname, '..', 'lib', 'agent-registry.js'), 'utf8',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Find the writeInterrupt function
|
|
94
|
+
const writeInterruptStart = registrySrc.indexOf('function writeInterrupt');
|
|
95
|
+
assert(writeInterruptStart !== -1, 'writeInterrupt function not found');
|
|
96
|
+
|
|
97
|
+
// Find the next function after writeInterrupt
|
|
98
|
+
const nextFuncStart = registrySrc.indexOf('\nfunction ', writeInterruptStart + 1);
|
|
99
|
+
const writeInterruptBody = registrySrc.substring(
|
|
100
|
+
writeInterruptStart,
|
|
101
|
+
nextFuncStart !== -1 ? nextFuncStart : writeInterruptStart + 2000,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// The mkdirSync block should NOT contain the EEXIST check
|
|
105
|
+
// Look for the pattern: mkdirSync followed by EEXIST within same try block
|
|
106
|
+
const mkdirIdx = writeInterruptBody.indexOf('mkdirSync');
|
|
107
|
+
assert(mkdirIdx !== -1, 'mkdirSync not found in writeInterrupt');
|
|
108
|
+
|
|
109
|
+
// Get the try/catch block around mkdirSync (next ~200 chars)
|
|
110
|
+
const mkdirBlock = writeInterruptBody.substring(mkdirIdx - 50, mkdirIdx + 200);
|
|
111
|
+
|
|
112
|
+
assert(
|
|
113
|
+
!mkdirBlock.includes("e.code !== 'EEXIST'"),
|
|
114
|
+
'writeInterrupt still contains dead EEXIST guard on mkdirSync',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('writeInterrupt still catches real mkdir errors', () => {
|
|
119
|
+
const registrySrc = fs.readFileSync(
|
|
120
|
+
path.join(__dirname, '..', 'lib', 'agent-registry.js'), 'utf8',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const writeInterruptStart = registrySrc.indexOf('function writeInterrupt');
|
|
124
|
+
const nextFuncStart = registrySrc.indexOf('\nfunction ', writeInterruptStart + 1);
|
|
125
|
+
const writeInterruptBody = registrySrc.substring(
|
|
126
|
+
writeInterruptStart,
|
|
127
|
+
nextFuncStart !== -1 ? nextFuncStart : writeInterruptStart + 2000,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Should still have error handling
|
|
131
|
+
assert(
|
|
132
|
+
writeInterruptBody.includes('Failed to create agent directory'),
|
|
133
|
+
'writeInterrupt lost its error message for mkdir failures',
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('writeInterrupt functional test - writes interrupt file correctly', () => {
|
|
138
|
+
// Skip if running in sandbox where fs.mkdirSync is not available
|
|
139
|
+
if (typeof fs.mkdirSync !== 'function') {
|
|
140
|
+
console.log(' (skipped - fs.mkdirSync not available in sandbox)');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const registry = require(path.join(__dirname, '..', 'lib', 'agent-registry.js'));
|
|
145
|
+
|
|
146
|
+
// Use setAgentsDir to override the cached directory path
|
|
147
|
+
const tmpBase = path.join(os.tmpdir(), 'pave-test-pr233-' + process.pid + '-' + Date.now());
|
|
148
|
+
const tmpAgentsDir = path.join(tmpBase, 'agents');
|
|
149
|
+
fs.mkdirSync(tmpAgentsDir, { recursive: true });
|
|
150
|
+
|
|
151
|
+
registry.setAgentsDir(tmpAgentsDir);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = registry.writeInterrupt('test-agent-pr233', 'test message', { action: 'stop' });
|
|
155
|
+
assert(result.success === true, 'writeInterrupt failed: ' + (result.error || 'unknown'));
|
|
156
|
+
|
|
157
|
+
// Verify the interrupt file exists and has correct content
|
|
158
|
+
const interruptPath = path.join(tmpAgentsDir, 'test-agent-pr233', 'interrupt.json');
|
|
159
|
+
assert(fs.existsSync(interruptPath), 'interrupt.json not created');
|
|
160
|
+
|
|
161
|
+
const data = JSON.parse(fs.readFileSync(interruptPath, 'utf8'));
|
|
162
|
+
assertEqual(data.action, 'stop');
|
|
163
|
+
assertEqual(data.message, 'test message');
|
|
164
|
+
assert(data.timestamp > 0, 'Missing timestamp');
|
|
165
|
+
} finally {
|
|
166
|
+
registry.resetAgentsDir();
|
|
167
|
+
try { fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (e) {}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// Test Suite 3: Fix 3 - writeStatus includes port field
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
console.log('\nTest Suite 3: Fix 3 - writeStatus includes port field');
|
|
176
|
+
|
|
177
|
+
test('WORKING writeStatus call includes port field', () => {
|
|
178
|
+
const indexSrc = fs.readFileSync(
|
|
179
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Find writeStatus with STATES.WORKING
|
|
183
|
+
const workingIdx = indexSrc.indexOf("state: registry.STATES.WORKING");
|
|
184
|
+
assert(workingIdx !== -1, 'WORKING writeStatus not found');
|
|
185
|
+
|
|
186
|
+
// Get the block around it (look for closing })
|
|
187
|
+
const blockEnd = indexSrc.indexOf('});', workingIdx);
|
|
188
|
+
const block = indexSrc.substring(workingIdx, blockEnd);
|
|
189
|
+
|
|
190
|
+
assert(block.includes('port: actualPort'),
|
|
191
|
+
'WORKING writeStatus missing port: actualPort field');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('SLEEPING writeStatus call includes port field', () => {
|
|
195
|
+
const indexSrc = fs.readFileSync(
|
|
196
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const sleepingIdx = indexSrc.indexOf("state: registry.STATES.SLEEPING");
|
|
200
|
+
assert(sleepingIdx !== -1, 'SLEEPING writeStatus not found');
|
|
201
|
+
|
|
202
|
+
const blockEnd = indexSrc.indexOf('});', sleepingIdx);
|
|
203
|
+
const block = indexSrc.substring(sleepingIdx, blockEnd);
|
|
204
|
+
|
|
205
|
+
assert(block.includes('port: actualPort'),
|
|
206
|
+
'SLEEPING writeStatus missing port: actualPort field');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('STOPPED writeStatus call includes port field', () => {
|
|
210
|
+
const indexSrc = fs.readFileSync(
|
|
211
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Find the second PORT definition (agent loop), not the first (server)
|
|
215
|
+
const portStr = "const PORT = args.port || parseInt(process.env.PAVE_PORT || '4096', 10);";
|
|
216
|
+
const firstPortIdx = indexSrc.indexOf(portStr);
|
|
217
|
+
assert(firstPortIdx !== -1, 'First PORT definition not found');
|
|
218
|
+
const portDefIdx = indexSrc.indexOf(portStr, firstPortIdx + portStr.length);
|
|
219
|
+
assert(portDefIdx !== -1, 'Agent loop PORT definition (second occurrence) not found');
|
|
220
|
+
|
|
221
|
+
const stoppedIdx = indexSrc.indexOf("state: registry.STATES.STOPPED", portDefIdx);
|
|
222
|
+
assert(stoppedIdx !== -1, 'STOPPED writeStatus after PORT def not found');
|
|
223
|
+
|
|
224
|
+
const blockEnd = indexSrc.indexOf('});', stoppedIdx);
|
|
225
|
+
const block = indexSrc.substring(stoppedIdx, blockEnd);
|
|
226
|
+
|
|
227
|
+
assert(block.includes('port: actualPort'),
|
|
228
|
+
'STOPPED writeStatus missing port: actualPort field');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('STARTING writeStatus does NOT need port (PORT not yet defined)', () => {
|
|
232
|
+
const indexSrc = fs.readFileSync(
|
|
233
|
+
path.join(__dirname, '..', 'index.js'), 'utf8',
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const startingIdx = indexSrc.indexOf("state: registry.STATES.STARTING");
|
|
237
|
+
assert(startingIdx !== -1, 'STARTING writeStatus not found');
|
|
238
|
+
|
|
239
|
+
const blockEnd = indexSrc.indexOf('});', startingIdx);
|
|
240
|
+
const block = indexSrc.substring(startingIdx, blockEnd);
|
|
241
|
+
|
|
242
|
+
// STARTING is called before the server starts, so port is not yet available
|
|
243
|
+
// This test documents that this is intentional
|
|
244
|
+
assert(!block.includes('port:'),
|
|
245
|
+
'STARTING writeStatus should not have port field (server not yet started)');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// Summary
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
console.log('\n' + '='.repeat(50));
|
|
253
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
|
|
254
|
+
if (failed > 0) {
|
|
255
|
+
console.log('SOME TESTS FAILED');
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
} else {
|
|
258
|
+
console.log('ALL TESTS PASSED');
|
|
259
|
+
}
|