@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,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple test runner for Jest-style tests in Node.js environment
|
|
3
|
+
* Provides basic describe/test/expect functionality with Jest compatibility
|
|
4
|
+
*
|
|
5
|
+
* NOTE: This runner works in sandboxed environments by mocking module requires
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Track hooks and test state
|
|
9
|
+
let currentSuiteHooks = { beforeEach: [], afterEach: [] };
|
|
10
|
+
let currentSuiteTests = [];
|
|
11
|
+
let isDefiningTests = false; // eslint-disable-line no-unused-vars
|
|
12
|
+
|
|
13
|
+
// Jest mock implementation
|
|
14
|
+
global.jest = {
|
|
15
|
+
spyOn(obj, method) {
|
|
16
|
+
const original = obj[method];
|
|
17
|
+
const calls = [];
|
|
18
|
+
const mockFn = function (...args) {
|
|
19
|
+
calls.push(args);
|
|
20
|
+
if (mockFn._mockImplementation) {
|
|
21
|
+
return mockFn._mockImplementation(...args);
|
|
22
|
+
}
|
|
23
|
+
return original ? original.apply(obj, args) : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
mockFn.mock = { calls };
|
|
27
|
+
mockFn.mockImplementation = function (impl) {
|
|
28
|
+
mockFn._mockImplementation = impl;
|
|
29
|
+
return mockFn;
|
|
30
|
+
};
|
|
31
|
+
mockFn.mockRestore = function () {
|
|
32
|
+
obj[method] = original;
|
|
33
|
+
};
|
|
34
|
+
mockFn.mockClear = function () {
|
|
35
|
+
calls.length = 0;
|
|
36
|
+
return mockFn;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
obj[method] = mockFn;
|
|
40
|
+
return mockFn;
|
|
41
|
+
},
|
|
42
|
+
fn(impl) {
|
|
43
|
+
const calls = [];
|
|
44
|
+
const mockFn = function (...args) {
|
|
45
|
+
calls.push(args);
|
|
46
|
+
if (impl) return impl(...args);
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
mockFn.mock = { calls };
|
|
50
|
+
mockFn.mockImplementation = function (newImpl) {
|
|
51
|
+
impl = newImpl;
|
|
52
|
+
return mockFn;
|
|
53
|
+
};
|
|
54
|
+
mockFn.mockClear = function () {
|
|
55
|
+
calls.length = 0;
|
|
56
|
+
return mockFn;
|
|
57
|
+
};
|
|
58
|
+
return mockFn;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// beforeEach hook
|
|
63
|
+
global.beforeEach = function (fn) {
|
|
64
|
+
currentSuiteHooks.beforeEach.push(fn);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// afterEach hook
|
|
68
|
+
global.afterEach = function (fn) {
|
|
69
|
+
currentSuiteHooks.afterEach.push(fn);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
global.describe = function (suiteName, suiteFunction) {
|
|
73
|
+
// Reset hooks for new suite
|
|
74
|
+
const previousHooks = { ...currentSuiteHooks };
|
|
75
|
+
currentSuiteHooks = { beforeEach: [], afterEach: [] };
|
|
76
|
+
currentSuiteTests = [];
|
|
77
|
+
|
|
78
|
+
console.log(`\n๐ ${suiteName}`);
|
|
79
|
+
|
|
80
|
+
// Set flag to indicate we're in definition phase
|
|
81
|
+
isDefiningTests = true;
|
|
82
|
+
|
|
83
|
+
// Collect tests
|
|
84
|
+
try {
|
|
85
|
+
suiteFunction();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.log(`โ ${suiteName} - Setup failed: ${error.message}`);
|
|
88
|
+
isDefiningTests = false;
|
|
89
|
+
currentSuiteHooks = previousHooks;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
isDefiningTests = false;
|
|
94
|
+
|
|
95
|
+
// Run tests with hooks
|
|
96
|
+
let passed = 0;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
|
|
99
|
+
for (const { testName, testFunction } of currentSuiteTests) {
|
|
100
|
+
try {
|
|
101
|
+
// Run beforeEach hooks
|
|
102
|
+
for (const hook of currentSuiteHooks.beforeEach) {
|
|
103
|
+
hook();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Run the test
|
|
107
|
+
testFunction();
|
|
108
|
+
|
|
109
|
+
// Run afterEach hooks
|
|
110
|
+
for (const hook of currentSuiteHooks.afterEach) {
|
|
111
|
+
try { hook(); } catch (e) { /* ignore cleanup errors */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(` โ ${testName}`);
|
|
115
|
+
passed++;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Still try to run afterEach even on failure
|
|
118
|
+
for (const hook of currentSuiteHooks.afterEach) {
|
|
119
|
+
try { hook(); } catch (e) { /* ignore cleanup errors */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(` โ ${testName} - ${error.message}`);
|
|
123
|
+
failed++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Restore previous hooks (for nested describes)
|
|
128
|
+
currentSuiteHooks = previousHooks;
|
|
129
|
+
|
|
130
|
+
if (failed > 0) {
|
|
131
|
+
console.log(`โ ${suiteName} - ${failed} test(s) failed`);
|
|
132
|
+
throw new Error(`Suite "${suiteName}" had ${failed} failing test(s)`);
|
|
133
|
+
} else if (passed > 0) {
|
|
134
|
+
console.log(`โ
${suiteName} - All ${passed} tests passed`);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
global.test = function (testName, testFunction) {
|
|
139
|
+
currentSuiteTests.push({ testName, testFunction });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Alias for test
|
|
143
|
+
global.it = global.test;
|
|
144
|
+
|
|
145
|
+
global.expect = function (actual) {
|
|
146
|
+
const matchers = {
|
|
147
|
+
toBe(expected) {
|
|
148
|
+
if (actual !== expected) {
|
|
149
|
+
throw new Error(`Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
toEqual(expected) {
|
|
153
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
154
|
+
throw new Error(`Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
toContain(expected) {
|
|
158
|
+
const arr = Array.isArray(actual) ? actual : String(actual);
|
|
159
|
+
if (!arr.includes(expected)) {
|
|
160
|
+
throw new Error(`Expected "${JSON.stringify(actual)}" to contain "${expected}"`);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
toHaveLength(expected) {
|
|
164
|
+
if (actual.length !== expected) {
|
|
165
|
+
throw new Error(`Expected length ${expected}, but got ${actual.length}`);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
toHaveProperty(prop) {
|
|
169
|
+
if (!actual || !Object.prototype.hasOwnProperty.call(actual, prop)) {
|
|
170
|
+
throw new Error(`Expected object to have property "${prop}"`);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
toBeGreaterThan(expected) {
|
|
174
|
+
if (actual <= expected) {
|
|
175
|
+
throw new Error(`Expected ${actual} to be greater than ${expected}`);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
toBeLessThan(expected) {
|
|
179
|
+
if (actual >= expected) {
|
|
180
|
+
throw new Error(`Expected ${actual} to be less than ${expected}`);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
toMatch(regex) {
|
|
184
|
+
if (typeof regex === 'string') {
|
|
185
|
+
regex = new RegExp(regex);
|
|
186
|
+
}
|
|
187
|
+
if (!regex.test(actual)) {
|
|
188
|
+
throw new Error(`Expected "${actual}" to match ${regex}`);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
toBeDefined() {
|
|
192
|
+
if (actual === undefined) {
|
|
193
|
+
throw new Error('Expected value to be defined');
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
toBeUndefined() {
|
|
197
|
+
if (actual !== undefined) {
|
|
198
|
+
throw new Error(`Expected undefined, but got ${JSON.stringify(actual)}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
toBeTruthy() {
|
|
202
|
+
if (!actual) {
|
|
203
|
+
throw new Error(`Expected truthy value, but got ${JSON.stringify(actual)}`);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
toBeFalsy() {
|
|
207
|
+
if (actual) {
|
|
208
|
+
throw new Error(`Expected falsy value, but got ${JSON.stringify(actual)}`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
toBeNull() {
|
|
212
|
+
if (actual !== null) {
|
|
213
|
+
throw new Error(`Expected null, but got ${JSON.stringify(actual)}`);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
toBeInstanceOf(expected) {
|
|
217
|
+
if (!(actual instanceof expected)) {
|
|
218
|
+
throw new Error(`Expected instance of ${expected.name}`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
toThrow(expected) {
|
|
222
|
+
let threw = false;
|
|
223
|
+
let error = null;
|
|
224
|
+
try {
|
|
225
|
+
actual();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
threw = true;
|
|
228
|
+
error = e;
|
|
229
|
+
}
|
|
230
|
+
if (!threw) {
|
|
231
|
+
throw new Error('Expected function to throw');
|
|
232
|
+
}
|
|
233
|
+
if (expected && !String(error.message).includes(expected)) {
|
|
234
|
+
throw new Error(`Expected error message to contain "${expected}", got "${error.message}"`);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
toHaveBeenCalled() {
|
|
238
|
+
if (!actual || !actual.mock || actual.mock.calls.length === 0) {
|
|
239
|
+
throw new Error('Expected function to have been called');
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
toHaveBeenCalledWith(...expectedArgs) {
|
|
243
|
+
if (!actual || !actual.mock) {
|
|
244
|
+
throw new Error('Expected a mock function');
|
|
245
|
+
}
|
|
246
|
+
const found = actual.mock.calls.some((call) => {
|
|
247
|
+
return expectedArgs.every((arg, i) => {
|
|
248
|
+
if (arg && typeof arg.asymmetricMatch === 'function') {
|
|
249
|
+
return arg.asymmetricMatch(call[i]);
|
|
250
|
+
}
|
|
251
|
+
return JSON.stringify(call[i]) === JSON.stringify(arg);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
if (!found) {
|
|
255
|
+
throw new Error(`Expected to have been called with ${JSON.stringify(expectedArgs)}, calls: ${JSON.stringify(actual.mock.calls)}`);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
toHaveBeenCalledTimes(expected) {
|
|
259
|
+
if (!actual || !actual.mock) {
|
|
260
|
+
throw new Error('Expected a mock function');
|
|
261
|
+
}
|
|
262
|
+
if (actual.mock.calls.length !== expected) {
|
|
263
|
+
throw new Error(`Expected to have been called ${expected} times, but was called ${actual.mock.calls.length} times`);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
not: {
|
|
267
|
+
toBe(expected) {
|
|
268
|
+
if (actual === expected) {
|
|
269
|
+
throw new Error(`Expected ${JSON.stringify(actual)} not to be ${JSON.stringify(expected)}`);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
toEqual(expected) {
|
|
273
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) {
|
|
274
|
+
throw new Error(`Expected ${JSON.stringify(actual)} not to equal ${JSON.stringify(expected)}`);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
toContain(expected) {
|
|
278
|
+
const arr = Array.isArray(actual) ? actual : String(actual);
|
|
279
|
+
if (arr.includes(expected)) {
|
|
280
|
+
throw new Error(`Expected "${JSON.stringify(actual)}" not to contain "${expected}"`);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
toHaveProperty(prop) {
|
|
284
|
+
if (actual && Object.prototype.hasOwnProperty.call(actual, prop)) {
|
|
285
|
+
throw new Error(`Expected object not to have property "${prop}"`);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
toHaveBeenCalled() {
|
|
289
|
+
if (actual && actual.mock && actual.mock.calls.length > 0) {
|
|
290
|
+
throw new Error(`Expected function not to have been called, but was called ${actual.mock.calls.length} times`);
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
toHaveBeenCalledWith(...expectedArgs) {
|
|
294
|
+
if (!actual || !actual.mock) {
|
|
295
|
+
throw new Error('Expected a mock function');
|
|
296
|
+
}
|
|
297
|
+
const found = actual.mock.calls.some((call) => {
|
|
298
|
+
return expectedArgs.every((arg, i) => {
|
|
299
|
+
if (arg && typeof arg.asymmetricMatch === 'function') {
|
|
300
|
+
return arg.asymmetricMatch(call[i]);
|
|
301
|
+
}
|
|
302
|
+
return JSON.stringify(call[i]) === JSON.stringify(arg);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
if (found) {
|
|
306
|
+
throw new Error(`Expected not to have been called with ${JSON.stringify(expectedArgs)}`);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
toBeDefined() {
|
|
310
|
+
if (actual !== undefined) {
|
|
311
|
+
throw new Error(`Expected undefined, but got ${JSON.stringify(actual)}`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
toBeNull() {
|
|
315
|
+
if (actual === null) {
|
|
316
|
+
throw new Error('Expected value not to be null');
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return matchers;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Static method for asymmetric string matching
|
|
326
|
+
global.expect.stringContaining = function (expected) {
|
|
327
|
+
return {
|
|
328
|
+
asymmetricMatch(actual) {
|
|
329
|
+
return typeof actual === 'string' && actual.includes(expected);
|
|
330
|
+
},
|
|
331
|
+
toString() {
|
|
332
|
+
return `StringContaining "${expected}"`;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// ========================================
|
|
338
|
+
// Mock implementations for local modules
|
|
339
|
+
// ========================================
|
|
340
|
+
|
|
341
|
+
// Mock parseArgs (from ../lib/args-parser)
|
|
342
|
+
// Default: showTools is true (tool output shown by default)
|
|
343
|
+
// Use --no-tools to disable tool output
|
|
344
|
+
function mockParseArgs(args) {
|
|
345
|
+
return {
|
|
346
|
+
command: args[0] || '',
|
|
347
|
+
commandArgs: args.slice(1).filter((arg) => !arg.startsWith('--') && !arg.startsWith('-')),
|
|
348
|
+
showTools: !args.includes('--no-tools'), // Default true, --no-tools disables
|
|
349
|
+
json: args.includes('--json'),
|
|
350
|
+
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
351
|
+
noStream: args.includes('--no-stream'),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function mockShowHelp() {
|
|
356
|
+
console.log('PAVE - Personal AI Virtual Environment');
|
|
357
|
+
console.log('');
|
|
358
|
+
console.log('Usage: pave [command] [options]');
|
|
359
|
+
console.log('');
|
|
360
|
+
console.log('Options:');
|
|
361
|
+
console.log(' --no-tools Hide tool execution progress/results (default: shown)');
|
|
362
|
+
console.log(' --json Output as JSON');
|
|
363
|
+
console.log(' --verbose, -v Verbose output');
|
|
364
|
+
console.log(' --no-stream Disable streaming output');
|
|
365
|
+
console.log('');
|
|
366
|
+
console.log('Examples:');
|
|
367
|
+
console.log(' pave chat "run bash ls"');
|
|
368
|
+
console.log(' pave chat --no-tools "summarize files"');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Mock extractToolCommand (from tool-display-utils)
|
|
372
|
+
function mockExtractToolCommand(toolName, input) {
|
|
373
|
+
if (!input || typeof input !== 'string') {
|
|
374
|
+
return toolName;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(input);
|
|
378
|
+
if (parsed.command) {
|
|
379
|
+
return `${toolName} ${parsed.command}`;
|
|
380
|
+
}
|
|
381
|
+
return toolName;
|
|
382
|
+
} catch {
|
|
383
|
+
return toolName;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Mock formatToolOutput
|
|
388
|
+
function mockFormatToolOutput(options) {
|
|
389
|
+
const { toolName, status, input: _input, output: _output, duration, error: _error, showTools } = options;
|
|
390
|
+
if (!showTools) return '';
|
|
391
|
+
|
|
392
|
+
let result = '';
|
|
393
|
+
const statusIcon = {
|
|
394
|
+
running: '๐ง',
|
|
395
|
+
completed: 'โ
',
|
|
396
|
+
error: 'โ',
|
|
397
|
+
}[status] || '๐ง';
|
|
398
|
+
|
|
399
|
+
result += `${statusIcon} ${toolName} ${status}`;
|
|
400
|
+
if (duration) result += ` (${duration}ms)`;
|
|
401
|
+
result += '\n';
|
|
402
|
+
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Mock escapeForShellBox
|
|
407
|
+
function mockEscapeForShellBox(text) {
|
|
408
|
+
if (!text) return '';
|
|
409
|
+
return String(text).replace(/[<>&"']/g, (c) => ({
|
|
410
|
+
'<': '<', '>': '>', '&': '&', '"': '"', "'": ''',
|
|
411
|
+
})[c] || c);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Mock TOOL_COLORS
|
|
415
|
+
const mockToolColors = {
|
|
416
|
+
reset: '\x1b[0m',
|
|
417
|
+
bold: '\x1b[1m',
|
|
418
|
+
dim: '\x1b[2m',
|
|
419
|
+
red: '\x1b[31m',
|
|
420
|
+
green: '\x1b[32m',
|
|
421
|
+
yellow: '\x1b[33m',
|
|
422
|
+
cyan: '\x1b[36m',
|
|
423
|
+
gray: '\x1b[90m',
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Mock BOX characters
|
|
427
|
+
const mockBOX = {
|
|
428
|
+
topLeft: 'โ',
|
|
429
|
+
topRight: 'โ',
|
|
430
|
+
bottomLeft: 'โ',
|
|
431
|
+
bottomRight: 'โ',
|
|
432
|
+
horizontal: 'โ',
|
|
433
|
+
vertical: 'โ',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Make mocks available globally for tests
|
|
437
|
+
global.mockModules = {
|
|
438
|
+
parseArgs: mockParseArgs,
|
|
439
|
+
showHelp: mockShowHelp,
|
|
440
|
+
extractToolCommand: mockExtractToolCommand,
|
|
441
|
+
formatToolOutput: mockFormatToolOutput,
|
|
442
|
+
escapeForShellBox: mockEscapeForShellBox,
|
|
443
|
+
TOOL_COLORS: mockToolColors,
|
|
444
|
+
BOX: mockBOX,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// Module mocking setup
|
|
448
|
+
const path = require('path');
|
|
449
|
+
const fs = require('fs');
|
|
450
|
+
|
|
451
|
+
// Handle Module requirement for mocking, but only if available
|
|
452
|
+
let Module; let
|
|
453
|
+
originalLoad;
|
|
454
|
+
try {
|
|
455
|
+
Module = require('module');
|
|
456
|
+
originalLoad = Module._load;
|
|
457
|
+
} catch (e) {
|
|
458
|
+
// Module not available in sandbox, skip mocking
|
|
459
|
+
// Only log in debug mode to avoid test output noise
|
|
460
|
+
if (process.env.DEBUG) {
|
|
461
|
+
console.log('[INFO] Module mocking not available in sandbox environment');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Only override Module._load if Module is available
|
|
466
|
+
if (Module && originalLoad) {
|
|
467
|
+
Module._load = function (request, parent, isMain) {
|
|
468
|
+
// Check if this is a local module require from a test file
|
|
469
|
+
// Use path.sep for cross-platform compatibility (Windows uses \test\, Unix uses /test/)
|
|
470
|
+
if (parent && parent.filename && parent.filename.includes(`${path.sep}test${path.sep}`)) {
|
|
471
|
+
// Allow certain test files to use the real args-parser (for agent command testing)
|
|
472
|
+
// Explicitly check for known test file patterns that need real modules
|
|
473
|
+
const filename = path.basename(parent.filename);
|
|
474
|
+
const realArgsParserTests = [
|
|
475
|
+
'args-parser.test.js', // Tests agent command parsing with real parser
|
|
476
|
+
'agent-parsing.test.js', // Tests agent command parsing
|
|
477
|
+
'agent-lifecycle.test.js', // Tests agent lifecycle (stop/logs/daemon) parsing
|
|
478
|
+
'agent-inbox.test.js', // Tests -a/--agent flag parsing with real parser
|
|
479
|
+
'real-args-parser.test.js', // Any future tests needing real parser
|
|
480
|
+
'soul-reinject.test.js', // Tests --reinject-interval parsing with real parser
|
|
481
|
+
'agent-update.test.js', // Tests pave agent update parsing and logic (#279)
|
|
482
|
+
];
|
|
483
|
+
const useRealModules = realArgsParserTests.includes(filename);
|
|
484
|
+
|
|
485
|
+
// Handle relative paths to lib modules
|
|
486
|
+
if (request === '../lib/args-parser' || request === '../lib/args-parser.js') {
|
|
487
|
+
if (useRealModules) {
|
|
488
|
+
// Use the real args-parser for agent command tests
|
|
489
|
+
return originalLoad.apply(this, arguments);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
parseArgs: mockParseArgs,
|
|
493
|
+
showHelp: mockShowHelp,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (request === '../lib/tool-output-formatter' || request === '../lib/tool-output-formatter.js') {
|
|
497
|
+
return {
|
|
498
|
+
formatToolOutput: mockFormatToolOutput,
|
|
499
|
+
extractToolCommand: mockExtractToolCommand,
|
|
500
|
+
TOOL_COLORS: mockToolColors,
|
|
501
|
+
BOX: mockBOX,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (request === '../../tui/lib/tool-display-utils' || request === '../../tui/lib/tool-display-utils.js') {
|
|
505
|
+
return {
|
|
506
|
+
extractToolCommand: mockExtractToolCommand,
|
|
507
|
+
escapeForShellBox: mockEscapeForShellBox,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// For all other requires, use the original
|
|
513
|
+
return originalLoad.apply(this, arguments);
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ========================================
|
|
518
|
+
// Run tests
|
|
519
|
+
// ========================================
|
|
520
|
+
|
|
521
|
+
// Create local aliases for test globals so they're accessible in eval scope.
|
|
522
|
+
// In the sandbox, global.describe is set but 'describe' as a bare identifier isn't
|
|
523
|
+
// accessible inside eval'd code. These locals fix that.
|
|
524
|
+
const describe = global.describe;
|
|
525
|
+
const it = global.it;
|
|
526
|
+
const test = global.test;
|
|
527
|
+
const expect = global.expect;
|
|
528
|
+
const jest = global.jest;
|
|
529
|
+
const beforeEach = global.beforeEach;
|
|
530
|
+
const afterEach = global.afterEach;
|
|
531
|
+
|
|
532
|
+
console.log('๐งช Running PAVE Test Suite with Jest compatibility...\n');
|
|
533
|
+
if (Module && originalLoad) {
|
|
534
|
+
console.log('Note: Using mocked modules for sandboxed environment');
|
|
535
|
+
} else {
|
|
536
|
+
console.log('Note: Module mocking unavailable, using real modules');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const testDir = __dirname;
|
|
540
|
+
const testFiles = fs.readdirSync(testDir)
|
|
541
|
+
.filter((file) => file.endsWith('.test.js') && file !== 'run-tests.js')
|
|
542
|
+
.sort();
|
|
543
|
+
|
|
544
|
+
console.log(`Found ${testFiles.length} test file(s): ${testFiles.join(', ')}\n`);
|
|
545
|
+
|
|
546
|
+
let totalPassed = 0;
|
|
547
|
+
let totalFailed = 0;
|
|
548
|
+
const failedFiles = [];
|
|
549
|
+
|
|
550
|
+
// Detect sandbox environment: if Module._load override isn't available,
|
|
551
|
+
// the sandbox's require() wraps modules in an IIFE where global.describe etc.
|
|
552
|
+
// aren't accessible as bare identifiers. In that case for files that fail,
|
|
553
|
+
// we retry by reading the file and eval'ing with globals injected.
|
|
554
|
+
const useSandboxFallback = !Module;
|
|
555
|
+
|
|
556
|
+
for (const testFile of testFiles) {
|
|
557
|
+
try {
|
|
558
|
+
console.log(`\n${'โ'.repeat(50)}`);
|
|
559
|
+
console.log(`๐ Running ${testFile}...`);
|
|
560
|
+
|
|
561
|
+
const testPath = path.join(testDir, testFile);
|
|
562
|
+
|
|
563
|
+
// Clear the module from cache if it exists
|
|
564
|
+
if (require.cache && require.cache[testPath]) {
|
|
565
|
+
delete require.cache[testPath];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Reset process.exitCode before each test file to prevent leaking
|
|
569
|
+
// between tests that use process.exitCode for their own failure tracking.
|
|
570
|
+
const _savedExitCode = process.exitCode;
|
|
571
|
+
process.exitCode = 0;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
require(testPath);
|
|
575
|
+
} catch (requireError) {
|
|
576
|
+
// In sandbox mode, if describe/it/expect/test aren't visible as bare identifiers,
|
|
577
|
+
// retry by reading the file and eval'ing with those globals injected as locals.
|
|
578
|
+
if (useSandboxFallback && /\b(describe|it|test|expect|jest|beforeEach|afterEach)\b.* is not defined/.test(requireError.message)) {
|
|
579
|
+
let testContent = fs.readFileSync(testPath, 'utf8');
|
|
580
|
+
// Strip shebang if present
|
|
581
|
+
if (testContent.substring(0, 2) === '#!') {
|
|
582
|
+
const nlIdx = testContent.indexOf('\n');
|
|
583
|
+
testContent = nlIdx !== -1 ? testContent.substring(nlIdx + 1) : '';
|
|
584
|
+
}
|
|
585
|
+
// Create a function with test globals as parameters
|
|
586
|
+
const fn = new Function(
|
|
587
|
+
'describe', 'it', 'test', 'expect', 'jest', 'beforeEach', 'afterEach',
|
|
588
|
+
'require', '__dirname', '__filename', 'module', 'exports',
|
|
589
|
+
'console', 'process', 'global', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Buffer',
|
|
590
|
+
testContent,
|
|
591
|
+
);
|
|
592
|
+
const modObj = { exports: {} };
|
|
593
|
+
fn(
|
|
594
|
+
describe, it, test, expect, jest, beforeEach, afterEach,
|
|
595
|
+
require, path.dirname(testPath), testPath, modObj, modObj.exports,
|
|
596
|
+
console, process, global, setTimeout, clearTimeout, setInterval, clearInterval,
|
|
597
|
+
typeof Buffer !== 'undefined' ? Buffer : undefined,
|
|
598
|
+
);
|
|
599
|
+
} else {
|
|
600
|
+
throw requireError;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check if the test file itself signaled failure via process.exitCode
|
|
605
|
+
if (process.exitCode === 1) {
|
|
606
|
+
process.exitCode = 0; // Reset so it doesn't affect the runner
|
|
607
|
+
throw new Error(`${testFile} signaled failure via process.exitCode`);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
totalPassed++;
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.error(`[ERROR] โ ${testFile} failed:`, error.message);
|
|
613
|
+
if (process.env.VERBOSE) {
|
|
614
|
+
console.error(error.stack);
|
|
615
|
+
}
|
|
616
|
+
failedFiles.push(testFile);
|
|
617
|
+
totalFailed++;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Always ensure process.exitCode is clean for the next file
|
|
621
|
+
process.exitCode = 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Restore original Module._load implementation
|
|
625
|
+
if (Module && originalLoad) {
|
|
626
|
+
Module._load = originalLoad;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
console.log(`\n${'โ'.repeat(50)}`);
|
|
630
|
+
console.log(`๐ Test Results:`);
|
|
631
|
+
console.log(` Passed: \x1b[32m${totalPassed}\x1b[0m file(s)`);
|
|
632
|
+
console.log(` Failed: \x1b[31m${totalFailed}\x1b[0m file(s)`);
|
|
633
|
+
|
|
634
|
+
if (failedFiles.length > 0) {
|
|
635
|
+
console.log(`\n Failed files:`);
|
|
636
|
+
failedFiles.forEach((f) => console.log(` - ${f}`));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (totalFailed > 0) {
|
|
640
|
+
process.exit(1);
|
|
641
|
+
} else {
|
|
642
|
+
console.log('\n๐ All tests passed!');
|
|
643
|
+
}
|