@cnrai/pave 0.3.32 → 0.3.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +5775 -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 -33
- 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 -2
- 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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for sandbox shell redirection stripping (Issue #127).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that shell redirections (2>&1, >/dev/null, etc.) are stripped
|
|
5
|
+
* from node command args before parsing, preventing them from being
|
|
6
|
+
* mistaken for script filenames.
|
|
7
|
+
*
|
|
8
|
+
* Uses checkmark/X format so run-all.js correctly counts results.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let passed = 0;
|
|
12
|
+
let failed = 0;
|
|
13
|
+
|
|
14
|
+
function assert(cond, msg) {
|
|
15
|
+
if (cond) {
|
|
16
|
+
passed++;
|
|
17
|
+
console.log('\u2705 ' + msg);
|
|
18
|
+
} else {
|
|
19
|
+
failed++;
|
|
20
|
+
console.log('\u274c ' + msg);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
// ---- Load stripShellRedirections ----
|
|
28
|
+
// Try require() first (works in CI where deps are installed).
|
|
29
|
+
// Fall back to source extraction when deps like js-yaml are missing.
|
|
30
|
+
const sandboxPath = path.resolve(__dirname, '../../opencode-lite/src/sandbox/SandboxRunner.js');
|
|
31
|
+
let stripShellRedirections = null;
|
|
32
|
+
try {
|
|
33
|
+
const sandboxModule = require(sandboxPath);
|
|
34
|
+
if (sandboxModule && typeof sandboxModule.stripShellRedirections === 'function') {
|
|
35
|
+
stripShellRedirections = sandboxModule.stripShellRedirections;
|
|
36
|
+
}
|
|
37
|
+
} catch (_requireErr) {
|
|
38
|
+
// Module has dependencies (js-yaml etc.) that may not be available.
|
|
39
|
+
// Extract just the standalone function from source as a fallback.
|
|
40
|
+
try {
|
|
41
|
+
const src = fs.readFileSync(sandboxPath, 'utf8');
|
|
42
|
+
// Match the function including its full body (handles nested braces
|
|
43
|
+
// by finding the function start and scanning for balanced braces).
|
|
44
|
+
const startIdx = src.indexOf('function stripShellRedirections(str)');
|
|
45
|
+
if (startIdx !== -1) {
|
|
46
|
+
let depth = 0;
|
|
47
|
+
let endIdx = startIdx;
|
|
48
|
+
let foundOpen = false;
|
|
49
|
+
for (let ci = startIdx; ci < src.length; ci++) {
|
|
50
|
+
if (src[ci] === '{') { depth++; foundOpen = true; }
|
|
51
|
+
if (src[ci] === '}') { depth--; }
|
|
52
|
+
if (foundOpen && depth === 0) { endIdx = ci + 1; break; }
|
|
53
|
+
}
|
|
54
|
+
const fnSrc = src.substring(startIdx, endIdx);
|
|
55
|
+
eval(fnSrc); // defines stripShellRedirections in this scope
|
|
56
|
+
}
|
|
57
|
+
} catch (_extractErr) {
|
|
58
|
+
stripShellRedirections = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!stripShellRedirections) {
|
|
63
|
+
throw new Error('Unable to load stripShellRedirections from ' + sandboxPath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- 1. Unit tests for stripShellRedirections ----
|
|
67
|
+
|
|
68
|
+
// Basic redirections (no space between operator and target)
|
|
69
|
+
assert(stripShellRedirections('--version 2>&1') === '--version',
|
|
70
|
+
'strip: --version 2>&1 -> --version');
|
|
71
|
+
assert(stripShellRedirections('--version') === '--version',
|
|
72
|
+
'strip: --version (no redirect) unchanged');
|
|
73
|
+
assert(stripShellRedirections('script.js 2>&1') === 'script.js',
|
|
74
|
+
'strip: script.js 2>&1 -> script.js');
|
|
75
|
+
assert(stripShellRedirections('script.js 2>/dev/null') === 'script.js',
|
|
76
|
+
'strip: script.js 2>/dev/null -> script.js');
|
|
77
|
+
assert(stripShellRedirections('script.js >/dev/null') === 'script.js',
|
|
78
|
+
'strip: script.js >/dev/null -> script.js');
|
|
79
|
+
assert(stripShellRedirections('script.js >output.txt') === 'script.js',
|
|
80
|
+
'strip: script.js >output.txt -> script.js');
|
|
81
|
+
assert(stripShellRedirections('script.js >>output.txt') === 'script.js',
|
|
82
|
+
'strip: script.js >>output.txt -> script.js');
|
|
83
|
+
assert(stripShellRedirections('script.js 2>>error.log') === 'script.js',
|
|
84
|
+
'strip: script.js 2>>error.log -> script.js');
|
|
85
|
+
assert(stripShellRedirections('script.js <input.txt') === 'script.js',
|
|
86
|
+
'strip: script.js <input.txt -> script.js');
|
|
87
|
+
assert(stripShellRedirections('script.js &>/dev/null') === 'script.js',
|
|
88
|
+
'strip: script.js &>/dev/null -> script.js');
|
|
89
|
+
assert(stripShellRedirections('script.js &>>output.txt') === 'script.js',
|
|
90
|
+
'strip: script.js &>>output.txt -> script.js');
|
|
91
|
+
|
|
92
|
+
// Whitespace-separated targets (Copilot review feedback)
|
|
93
|
+
assert(stripShellRedirections('script.js > out.txt') === 'script.js',
|
|
94
|
+
'strip: script.js > out.txt -> script.js (space-separated)');
|
|
95
|
+
assert(stripShellRedirections('script.js 2> /dev/null') === 'script.js',
|
|
96
|
+
'strip: script.js 2> /dev/null -> script.js (space-separated)');
|
|
97
|
+
assert(stripShellRedirections('script.js < input.txt') === 'script.js',
|
|
98
|
+
'strip: script.js < input.txt -> script.js (space-separated)');
|
|
99
|
+
assert(stripShellRedirections('script.js >> out.txt') === 'script.js',
|
|
100
|
+
'strip: script.js >> out.txt -> script.js (space-separated)');
|
|
101
|
+
assert(stripShellRedirections('script.js 2>> error.log') === 'script.js',
|
|
102
|
+
'strip: script.js 2>> error.log -> script.js (space-separated)');
|
|
103
|
+
assert(stripShellRedirections('script.js &> /dev/null') === 'script.js',
|
|
104
|
+
'strip: script.js &> /dev/null -> script.js (space-separated)');
|
|
105
|
+
assert(stripShellRedirections('script.js &>> out.txt') === 'script.js',
|
|
106
|
+
'strip: script.js &>> out.txt -> script.js (space-separated)');
|
|
107
|
+
|
|
108
|
+
// Multiple redirections
|
|
109
|
+
assert(stripShellRedirections('script.js 2>&1 >/dev/null') === 'script.js',
|
|
110
|
+
'strip: multiple redirections');
|
|
111
|
+
assert(stripShellRedirections('script.js >out.txt 2>&1') === 'script.js',
|
|
112
|
+
'strip: >out.txt 2>&1');
|
|
113
|
+
assert(stripShellRedirections('script.js > out.txt 2>&1') === 'script.js',
|
|
114
|
+
'strip: > out.txt 2>&1 (space-separated + fd redirect)');
|
|
115
|
+
|
|
116
|
+
// With script arguments
|
|
117
|
+
assert(stripShellRedirections('script.js --flag value 2>&1') === 'script.js --flag value',
|
|
118
|
+
'strip: preserves script args');
|
|
119
|
+
assert(stripShellRedirections('-e "console.log(1)" 2>&1') === '-e "console.log(1)"',
|
|
120
|
+
'strip: preserves -e inline code');
|
|
121
|
+
|
|
122
|
+
// Edge cases
|
|
123
|
+
assert(stripShellRedirections('2>&1') === '',
|
|
124
|
+
'strip: only redirection -> empty');
|
|
125
|
+
assert(stripShellRedirections('') === '',
|
|
126
|
+
'strip: empty string -> empty');
|
|
127
|
+
assert(stripShellRedirections('script.js') === 'script.js',
|
|
128
|
+
'strip: no redirection -> unchanged');
|
|
129
|
+
|
|
130
|
+
// FD numbers
|
|
131
|
+
assert(stripShellRedirections('script.js 1>&2') === 'script.js',
|
|
132
|
+
'strip: 1>&2');
|
|
133
|
+
assert(stripShellRedirections('script.js >&2') === 'script.js',
|
|
134
|
+
'strip: >&2');
|
|
135
|
+
|
|
136
|
+
// Quoted redirections must be preserved (Copilot review round 4)
|
|
137
|
+
assert(stripShellRedirections('-e "console.log(\'2>&1\')" 2>&1') === '-e "console.log(\'2>&1\')"',
|
|
138
|
+
'strip: preserves 2>&1 inside double quotes');
|
|
139
|
+
assert(stripShellRedirections("-e 'echo 2>/dev/null' 2>&1") === "-e 'echo 2>/dev/null'",
|
|
140
|
+
'strip: preserves 2>/dev/null inside single quotes');
|
|
141
|
+
assert(stripShellRedirections('-e "a < b" >/dev/null') === '-e "a < b"',
|
|
142
|
+
'strip: preserves < inside double quotes');
|
|
143
|
+
assert(stripShellRedirections("-e 'a > b' 2>&1") === "-e 'a > b'",
|
|
144
|
+
'strip: preserves > inside single quotes');
|
|
145
|
+
assert(stripShellRedirections('script.js "arg with 2>&1" 2>/dev/null') === 'script.js "arg with 2>&1"',
|
|
146
|
+
'strip: preserves 2>&1 in quoted script arg');
|
|
147
|
+
|
|
148
|
+
// Non-redirection uses of >/< in arguments must be preserved (Copilot review round 5)
|
|
149
|
+
assert(stripShellRedirections('-e "console.log(1>2)"') === '-e "console.log(1>2)"',
|
|
150
|
+
'strip: preserves > in quoted expression');
|
|
151
|
+
assert(stripShellRedirections("-e 'x=a<b'") === "-e 'x=a<b'",
|
|
152
|
+
'strip: preserves < in quoted expression');
|
|
153
|
+
|
|
154
|
+
// Backslash-escaped quotes inside double quotes (Copilot review round 6)
|
|
155
|
+
assert(stripShellRedirections('-e "console.log(\\"2>&1\\")" 2>&1') === '-e "console.log(\\"2>&1\\")"',
|
|
156
|
+
'strip: preserves escaped quotes with redirection inside');
|
|
157
|
+
|
|
158
|
+
// Digit-prefixed args should not lose digits (Copilot review round 6)
|
|
159
|
+
assert(stripShellRedirections('script.js --port 3000 2>&1') === 'script.js --port 3000',
|
|
160
|
+
'strip: preserves numeric arg followed by redirection');
|
|
161
|
+
|
|
162
|
+
// Bare & and && must not be treated as redirections (Copilot review round 7)
|
|
163
|
+
assert(stripShellRedirections('script.js && echo done') === 'script.js && echo done',
|
|
164
|
+
'strip: preserves && operator');
|
|
165
|
+
assert(stripShellRedirections('script.js &') === 'script.js &',
|
|
166
|
+
'strip: preserves background &');
|
|
167
|
+
|
|
168
|
+
// ---- 2. Integration flow tests ----
|
|
169
|
+
// Verify end-to-end that redirections don't pollute scriptPath detection.
|
|
170
|
+
function findScriptPath(fullArgs) {
|
|
171
|
+
const cleaned = stripShellRedirections(fullArgs);
|
|
172
|
+
const args = cleaned ? cleaned.split(/\s+/) : [];
|
|
173
|
+
let scriptPath = null;
|
|
174
|
+
for (let i = 0; i < args.length; i++) {
|
|
175
|
+
if (args[i].charAt(0) !== '-') {
|
|
176
|
+
scriptPath = args[i];
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return scriptPath;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
assert(findScriptPath('--version 2>&1') === null,
|
|
184
|
+
'flow: --version 2>&1 -> scriptPath is null');
|
|
185
|
+
assert(findScriptPath('script.js 2>&1') === 'script.js',
|
|
186
|
+
'flow: script.js 2>&1 -> scriptPath is script.js');
|
|
187
|
+
assert(findScriptPath('script.js 2>/dev/null') === 'script.js',
|
|
188
|
+
'flow: script.js 2>/dev/null -> scriptPath is script.js');
|
|
189
|
+
assert(findScriptPath('2>&1') === null,
|
|
190
|
+
'flow: just 2>&1 -> scriptPath is null');
|
|
191
|
+
assert(findScriptPath('-e 2>&1') === null,
|
|
192
|
+
'flow: -e 2>&1 -> scriptPath is null (-e is an option)');
|
|
193
|
+
assert(findScriptPath('--version') === null,
|
|
194
|
+
'flow: --version -> scriptPath is null');
|
|
195
|
+
assert(findScriptPath('--version > out.txt') === null,
|
|
196
|
+
'flow: --version > out.txt -> scriptPath is null');
|
|
197
|
+
assert(findScriptPath('--version 2> /dev/null') === null,
|
|
198
|
+
'flow: --version 2> /dev/null -> scriptPath is null');
|
|
199
|
+
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log(passed + ' passed, ' + failed + ' failed');
|
|
202
|
+
if (failed > 0) throw new Error(failed + ' test(s) failed');
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for agent session file path (issues #108, #174)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that both chat and agent commands use resolveSessionFile()
|
|
5
|
+
* to get the session file path, which resolves to .pave/session.json
|
|
6
|
+
* (migrating from .pave-session.json if needed).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
// Helper: extract a named function body from source using brace matching.
|
|
13
|
+
function extractFunctionBody(src, funcName) {
|
|
14
|
+
let idx = src.indexOf('async function ' + funcName);
|
|
15
|
+
if (idx === -1) idx = src.indexOf('function ' + funcName);
|
|
16
|
+
if (idx === -1) throw new Error('function ' + funcName + ' not found');
|
|
17
|
+
|
|
18
|
+
const braceStart = src.indexOf('{', idx);
|
|
19
|
+
if (braceStart === -1) throw new Error('Opening brace not found for ' + funcName);
|
|
20
|
+
|
|
21
|
+
let braceCount = 0;
|
|
22
|
+
let endIndex = braceStart;
|
|
23
|
+
for (let i = braceStart; i < src.length; i++) {
|
|
24
|
+
if (src[i] === '{') braceCount++;
|
|
25
|
+
else if (src[i] === '}') {
|
|
26
|
+
braceCount--;
|
|
27
|
+
if (braceCount === 0) { endIndex = i + 1; break; }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return src.slice(braceStart, endIndex);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Session file path (issues #108, #174)', () => {
|
|
34
|
+
const srcPath = path.join(__dirname, '..', 'index.js');
|
|
35
|
+
const src = fs.readFileSync(srcPath, 'utf8');
|
|
36
|
+
const agentBody = extractFunctionBody(src, 'handleAgentCommand');
|
|
37
|
+
const chatBody = extractFunctionBody(src, 'handleChatCommand');
|
|
38
|
+
|
|
39
|
+
// =========================================================================
|
|
40
|
+
// Test: resolveSessionFile helper exists
|
|
41
|
+
// =========================================================================
|
|
42
|
+
|
|
43
|
+
test('resolveSessionFile helper function exists', () => {
|
|
44
|
+
expect(src).toContain('function resolveSessionFile(');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// =========================================================================
|
|
48
|
+
// Test: SESSION_FILE in handleAgentCommand uses resolveSessionFile
|
|
49
|
+
// =========================================================================
|
|
50
|
+
|
|
51
|
+
test('agent SESSION_FILE uses resolveSessionFile', () => {
|
|
52
|
+
expect(agentBody).toContain('resolveSessionFile(configDir)');
|
|
53
|
+
expect(agentBody).not.toContain('path.join(process.cwd(), \'.pave-session.json\')');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// =========================================================================
|
|
57
|
+
// Test: SESSION_FILE in handleChatCommand uses resolveSessionFile
|
|
58
|
+
// =========================================================================
|
|
59
|
+
|
|
60
|
+
test('chat SESSION_FILE uses resolveSessionFile', () => {
|
|
61
|
+
expect(chatBody).toContain('resolveSessionFile(configDir)');
|
|
62
|
+
expect(chatBody).not.toContain('path.join(process.cwd(), \'.pave-session.json\')');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// Test: SESSION_FILE does not reference AGENT_LOG_DIR
|
|
67
|
+
// =========================================================================
|
|
68
|
+
|
|
69
|
+
test('agent SESSION_FILE does not reference AGENT_LOG_DIR', () => {
|
|
70
|
+
const codeOnly = agentBody.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
71
|
+
const assignRegex = /SESSION_FILE\s*=/g;
|
|
72
|
+
let m;
|
|
73
|
+
while ((m = assignRegex.exec(codeOnly)) !== null) {
|
|
74
|
+
const start = m.index;
|
|
75
|
+
let end = codeOnly.indexOf(';', start);
|
|
76
|
+
if (end === -1) end = codeOnly.length;
|
|
77
|
+
const stmt = codeOnly.slice(start, end);
|
|
78
|
+
expect(stmt).not.toContain('AGENT_LOG_DIR');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Test: both chat and agent use the same resolveSessionFile pattern
|
|
84
|
+
// =========================================================================
|
|
85
|
+
|
|
86
|
+
test('chat and agent both use resolveSessionFile(configDir)', () => {
|
|
87
|
+
expect(chatBody).toContain('resolveSessionFile(configDir)');
|
|
88
|
+
expect(agentBody).toContain('resolveSessionFile(configDir)');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// =========================================================================
|
|
92
|
+
// Test: no CWD_SESSION_FILE variable in source
|
|
93
|
+
// =========================================================================
|
|
94
|
+
|
|
95
|
+
test('no CWD_SESSION_FILE variable in source code', () => {
|
|
96
|
+
expect(src.indexOf('CWD_SESSION_FILE')).toBe(-1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// =========================================================================
|
|
100
|
+
// Test: no agent-session reference in source code
|
|
101
|
+
// =========================================================================
|
|
102
|
+
|
|
103
|
+
test('no agent-session reference in agent code', () => {
|
|
104
|
+
const codeOnly = agentBody.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
105
|
+
expect(codeOnly.indexOf('agent-session')).toBe(-1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// =========================================================================
|
|
109
|
+
// Test: no dual writes in handleAgentCommand
|
|
110
|
+
// =========================================================================
|
|
111
|
+
|
|
112
|
+
test('no dual writes to SESSION_FILE and CWD_SESSION_FILE', () => {
|
|
113
|
+
const hasSessionWrite = agentBody.includes('writeFileSync(SESSION_FILE');
|
|
114
|
+
const hasCwdSessionWrite = agentBody.includes('writeFileSync(CWD_SESSION_FILE');
|
|
115
|
+
|
|
116
|
+
if (hasSessionWrite && hasCwdSessionWrite) {
|
|
117
|
+
throw new Error('Found dual write to SESSION_FILE and CWD_SESSION_FILE');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// Test: resolveSessionFile handles migration
|
|
123
|
+
// =========================================================================
|
|
124
|
+
|
|
125
|
+
test('resolveSessionFile migrates .pave-session.json', () => {
|
|
126
|
+
const resolveBody = extractFunctionBody(src, 'resolveSessionFile');
|
|
127
|
+
expect(resolveBody).toContain('.pave-session.json');
|
|
128
|
+
expect(resolveBody).toContain('session.json');
|
|
129
|
+
expect(resolveBody).toContain('copyFileSync');
|
|
130
|
+
expect(resolveBody).toContain('unlinkSync');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for sandbox shebang stripping (Issue #129).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that shebang lines (#!/usr/bin/env node, etc.) are stripped
|
|
5
|
+
* from module content before eval() in the sandbox's __loadModule function.
|
|
6
|
+
*
|
|
7
|
+
* Uses checkmark/X format so run-all.js correctly counts results.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let passed = 0;
|
|
11
|
+
let failed = 0;
|
|
12
|
+
|
|
13
|
+
function assert(cond, msg) {
|
|
14
|
+
if (cond) {
|
|
15
|
+
passed++;
|
|
16
|
+
console.log('\u2705 ' + msg);
|
|
17
|
+
} else {
|
|
18
|
+
failed++;
|
|
19
|
+
console.log('\u274c ' + msg);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertEqual(actual, expected, msg) {
|
|
24
|
+
if (actual === expected) {
|
|
25
|
+
passed++;
|
|
26
|
+
console.log('\u2705 ' + msg);
|
|
27
|
+
} else {
|
|
28
|
+
failed++;
|
|
29
|
+
console.log('\u274c ' + msg);
|
|
30
|
+
console.log(' Expected: ' + JSON.stringify(expected));
|
|
31
|
+
console.log(' Actual: ' + JSON.stringify(actual));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
// ---- Verify shebang stripping code exists in generated sandbox APIs ----
|
|
39
|
+
|
|
40
|
+
const sandboxPath = path.resolve(__dirname, '../../opencode-lite/src/sandbox/SandboxRunner.js');
|
|
41
|
+
const src = fs.readFileSync(sandboxPath, 'utf8');
|
|
42
|
+
|
|
43
|
+
// Find the __loadModule function in the template literal
|
|
44
|
+
const loadModuleIdx = src.indexOf('function __loadModule(resolvedPath)');
|
|
45
|
+
assert(loadModuleIdx !== -1, 'source: __loadModule function exists in SandboxRunner.js');
|
|
46
|
+
|
|
47
|
+
// Verify shebang stripping code is present after __loadModule
|
|
48
|
+
const afterLoadModule = src.substring(loadModuleIdx, loadModuleIdx + 2000);
|
|
49
|
+
assert(afterLoadModule.indexOf('#!') !== -1,
|
|
50
|
+
'source: shebang check (#!) exists in __loadModule');
|
|
51
|
+
assert(afterLoadModule.indexOf('substring(0, 2)') !== -1,
|
|
52
|
+
'source: uses substring(0, 2) to check first two chars');
|
|
53
|
+
assert(afterLoadModule.indexOf('indexOf') !== -1,
|
|
54
|
+
'source: uses indexOf to find newline position');
|
|
55
|
+
|
|
56
|
+
// Verify the stripping happens BEFORE the eval wrapper
|
|
57
|
+
const evalIdx = afterLoadModule.indexOf('eval(wrapper)');
|
|
58
|
+
const shebangIdx = afterLoadModule.indexOf('#!');
|
|
59
|
+
assert(shebangIdx < evalIdx,
|
|
60
|
+
'source: shebang stripping occurs before eval(wrapper)');
|
|
61
|
+
|
|
62
|
+
// ---- Unit tests for shebang stripping logic ----
|
|
63
|
+
// The logic in __loadModule is:
|
|
64
|
+
// if (content.substring(0, 2) === '#!') {
|
|
65
|
+
// var nlIdx = content.indexOf('\n');
|
|
66
|
+
// content = nlIdx !== -1 ? content.substring(nlIdx + 1) : '';
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
function stripShebang(content) {
|
|
70
|
+
if (content.substring(0, 2) === '#!') {
|
|
71
|
+
const nlIdx = content.indexOf('\n');
|
|
72
|
+
content = nlIdx !== -1 ? content.substring(nlIdx + 1) : '';
|
|
73
|
+
}
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Basic shebang stripping
|
|
78
|
+
assertEqual(
|
|
79
|
+
stripShebang('#!/usr/bin/env node\nvar x = 1;'),
|
|
80
|
+
'var x = 1;',
|
|
81
|
+
'strip: #!/usr/bin/env node shebang is removed',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
assertEqual(
|
|
85
|
+
stripShebang('#!/usr/bin/node\nvar x = 1;'),
|
|
86
|
+
'var x = 1;',
|
|
87
|
+
'strip: #!/usr/bin/node shebang is removed',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assertEqual(
|
|
91
|
+
stripShebang('#!/bin/sh\necho hello'),
|
|
92
|
+
'echo hello',
|
|
93
|
+
'strip: #!/bin/sh shebang is removed',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Shebang with flags
|
|
97
|
+
assertEqual(
|
|
98
|
+
stripShebang('#!/usr/bin/env node --harmony\nvar x = 1;'),
|
|
99
|
+
'var x = 1;',
|
|
100
|
+
'strip: shebang with flags is removed',
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// No shebang - content unchanged
|
|
104
|
+
assertEqual(
|
|
105
|
+
stripShebang('var x = 1;'),
|
|
106
|
+
'var x = 1;',
|
|
107
|
+
'strip: no shebang - content unchanged',
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assertEqual(
|
|
111
|
+
stripShebang('// comment\nvar x = 1;'),
|
|
112
|
+
'// comment\nvar x = 1;',
|
|
113
|
+
'strip: comment line - content unchanged',
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assertEqual(
|
|
117
|
+
stripShebang(''),
|
|
118
|
+
'',
|
|
119
|
+
'strip: empty string - content unchanged',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Single character strings
|
|
123
|
+
assertEqual(
|
|
124
|
+
stripShebang('#'),
|
|
125
|
+
'#',
|
|
126
|
+
'strip: single # - content unchanged (not a shebang)',
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
assertEqual(
|
|
130
|
+
stripShebang('!'),
|
|
131
|
+
'!',
|
|
132
|
+
'strip: single ! - content unchanged',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Hash but not shebang
|
|
136
|
+
assertEqual(
|
|
137
|
+
stripShebang('# This is a comment\nvar x = 1;'),
|
|
138
|
+
'# This is a comment\nvar x = 1;',
|
|
139
|
+
'strip: # comment (not #!) - content unchanged',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Multiline content after shebang
|
|
143
|
+
assertEqual(
|
|
144
|
+
stripShebang('#!/usr/bin/env node\nvar x = 1;\nvar y = 2;\nconsole.log(x + y);'),
|
|
145
|
+
'var x = 1;\nvar y = 2;\nconsole.log(x + y);',
|
|
146
|
+
'strip: multiline content after shebang preserved',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Shebang-like content NOT at start
|
|
150
|
+
assertEqual(
|
|
151
|
+
stripShebang('var x = "#!/usr/bin/env node";\nvar y = 1;'),
|
|
152
|
+
'var x = "#!/usr/bin/env node";\nvar y = 1;',
|
|
153
|
+
'strip: #! not at start of file - content unchanged',
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Windows-style line endings (CRLF)
|
|
157
|
+
assertEqual(
|
|
158
|
+
stripShebang('#!/usr/bin/env node\r\nvar x = 1;'),
|
|
159
|
+
'var x = 1;',
|
|
160
|
+
'strip: CRLF line endings - shebang (including \\r) is fully removed',
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Shebang only (no content after)
|
|
164
|
+
assertEqual(
|
|
165
|
+
stripShebang('#!/usr/bin/env node\n'),
|
|
166
|
+
'',
|
|
167
|
+
'strip: shebang only with trailing newline - returns empty',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Shebang without newline (just the shebang, no terminator)
|
|
171
|
+
// With the guard, this correctly returns empty string since
|
|
172
|
+
// there's no actual code after the shebang.
|
|
173
|
+
const noNewline = stripShebang('#!/usr/bin/env node');
|
|
174
|
+
assertEqual(
|
|
175
|
+
noNewline,
|
|
176
|
+
'',
|
|
177
|
+
'strip: shebang without newline - returns empty string',
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ---- Verify args-parser.js has a shebang (the original trigger) ----
|
|
181
|
+
const argsParserPath = path.resolve(__dirname, '../lib/args-parser.js');
|
|
182
|
+
if (fs.existsSync(argsParserPath)) {
|
|
183
|
+
const argsParserContent = fs.readFileSync(argsParserPath, 'utf8');
|
|
184
|
+
assert(argsParserContent.substring(0, 2) === '#!',
|
|
185
|
+
'args-parser.js: starts with shebang (original trigger for issue #129)');
|
|
186
|
+
const strippedContent = stripShebang(argsParserContent);
|
|
187
|
+
assert(strippedContent.substring(0, 2) !== '#!',
|
|
188
|
+
'args-parser.js: shebang removed after stripShebang');
|
|
189
|
+
assert(strippedContent.length > 0,
|
|
190
|
+
'args-parser.js: content remains after shebang removal');
|
|
191
|
+
// Verify the remaining content starts with comment or code
|
|
192
|
+
assert(strippedContent.trimStart().indexOf('/**') === 0 || strippedContent.trimStart().indexOf('//') === 0 || strippedContent.trimStart().indexOf('var ') === 0 || strippedContent.trimStart().indexOf('const ') === 0,
|
|
193
|
+
'args-parser.js: content after shebang starts with valid JS');
|
|
194
|
+
} else {
|
|
195
|
+
console.log('(skipped args-parser.js verification - file not found at ' + argsParserPath + ')');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- Verify the wrapper would be valid JS after shebang stripping ----
|
|
199
|
+
// Simulates what __loadModule does: wrap content in a function and eval it
|
|
200
|
+
function simulateModuleLoad(content) {
|
|
201
|
+
// Apply shebang stripping (matches __loadModule implementation)
|
|
202
|
+
if (content.substring(0, 2) === '#!') {
|
|
203
|
+
const nlIdx = content.indexOf('\n');
|
|
204
|
+
content = nlIdx !== -1 ? content.substring(nlIdx + 1) : '';
|
|
205
|
+
}
|
|
206
|
+
// Create wrapper like __loadModule does
|
|
207
|
+
const wrapper = '(function(module, exports, require, __dirname, __filename) {' + content + '\n});';
|
|
208
|
+
// Try to parse (don't execute) - use Function constructor as a syntax check
|
|
209
|
+
try {
|
|
210
|
+
new Function(wrapper);
|
|
211
|
+
return { ok: true };
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return { ok: false, error: e.message };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result1 = simulateModuleLoad('#!/usr/bin/env node\nvar x = 1; module.exports = x;');
|
|
218
|
+
assert(result1.ok, 'wrapper: module with shebang parses after stripping');
|
|
219
|
+
|
|
220
|
+
const result2 = simulateModuleLoad('var x = 1; module.exports = x;');
|
|
221
|
+
assert(result2.ok, 'wrapper: module without shebang parses normally');
|
|
222
|
+
|
|
223
|
+
const result3 = simulateModuleLoad('#!/usr/bin/env node\n"use strict";\nvar x = 1;');
|
|
224
|
+
assert(result3.ok, 'wrapper: strict mode module with shebang parses after stripping');
|
|
225
|
+
|
|
226
|
+
// Shebang-only file without newline should now produce a valid (empty) wrapper
|
|
227
|
+
const result4 = simulateModuleLoad('#!/usr/bin/env node');
|
|
228
|
+
assert(result4.ok, 'wrapper: shebang-only file (no newline) produces valid empty wrapper');
|
|
229
|
+
|
|
230
|
+
// Without stripping, a shebang would cause SyntaxError
|
|
231
|
+
try {
|
|
232
|
+
const badWrapper = '(function(module, exports, require, __dirname, __filename) {#!/usr/bin/env node\nvar x = 1;\n});';
|
|
233
|
+
new Function(badWrapper);
|
|
234
|
+
assert(false, 'wrapper: unstripped shebang should cause SyntaxError');
|
|
235
|
+
} catch (e) {
|
|
236
|
+
assert(e instanceof SyntaxError, 'wrapper: unstripped shebang causes SyntaxError (confirms the bug)');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- Summary ----
|
|
240
|
+
console.log('\n' + (passed + failed) + ' tests: ' + passed + ' passed, ' + failed + ' failed');
|
|
241
|
+
if (failed > 0) process.exit(1);
|