@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.
Files changed (83) hide show
  1. package/MARKETPLACE.md +406 -0
  2. package/README.md +218 -21
  3. package/build-binary.js +591 -0
  4. package/build-npm.js +537 -0
  5. package/build.js +230 -0
  6. package/check-binary.js +26 -0
  7. package/deploy.sh +95 -0
  8. package/index.js +5775 -0
  9. package/lib/agent-registry.js +1037 -0
  10. package/lib/args-parser.js +837 -0
  11. package/lib/blessed-widget-patched.js +93 -0
  12. package/lib/cli-markdown.js +590 -0
  13. package/lib/compaction.js +153 -0
  14. package/lib/duration.js +94 -0
  15. package/lib/hash.js +22 -0
  16. package/lib/marketplace.js +866 -0
  17. package/lib/memory-config.js +166 -0
  18. package/lib/skill-manager.js +891 -0
  19. package/lib/soul.js +31 -0
  20. package/lib/tool-output-formatter.js +180 -0
  21. package/package.json +35 -33
  22. package/start-pave.sh +149 -0
  23. package/status.js +271 -0
  24. package/test/abort-stream.test.js +445 -0
  25. package/test/agent-auto-compaction.test.js +552 -0
  26. package/test/agent-comm-abort.test.js +95 -0
  27. package/test/agent-comm.test.js +598 -0
  28. package/test/agent-inbox.test.js +576 -0
  29. package/test/agent-init.test.js +264 -0
  30. package/test/agent-interrupt.test.js +314 -0
  31. package/test/agent-lifecycle.test.js +520 -0
  32. package/test/agent-log-files.test.js +349 -0
  33. package/test/agent-mode.manual-test.js +392 -0
  34. package/test/agent-parsing.test.js +228 -0
  35. package/test/agent-post-stream-idle.test.js +762 -0
  36. package/test/agent-registry.test.js +359 -0
  37. package/test/agent-rm.test.js +442 -0
  38. package/test/agent-spawn.test.js +933 -0
  39. package/test/agent-status-api.test.js +624 -0
  40. package/test/agent-update.test.js +435 -0
  41. package/test/args-parser.test.js +391 -0
  42. package/test/auto-compaction-chat.manual-test.js +227 -0
  43. package/test/auto-compaction.test.js +941 -0
  44. package/test/build-config.test.js +120 -0
  45. package/test/build-npm.test.js +388 -0
  46. package/test/chat-command.test.js +137 -0
  47. package/test/chat-leading-lines.test.js +159 -0
  48. package/test/config-flag.test.js +272 -0
  49. package/test/cursor-drift.test.js +135 -0
  50. package/test/debug-require.js +23 -0
  51. package/test/dir-migration.test.js +323 -0
  52. package/test/duration.test.js +229 -0
  53. package/test/ghostty-term.test.js +202 -0
  54. package/test/http500-backoff.test.js +854 -0
  55. package/test/integration.test.js +86 -0
  56. package/test/memory-guard-env.test.js +220 -0
  57. package/test/pr233-fixes.test.js +259 -0
  58. package/test/run-agent-init.js +297 -0
  59. package/test/run-all.js +64 -0
  60. package/test/run-config-flag.js +159 -0
  61. package/test/run-cursor-drift.js +82 -0
  62. package/test/run-session-path.js +154 -0
  63. package/test/run-tests.js +643 -0
  64. package/test/sandbox-redirect.test.js +202 -0
  65. package/test/session-path.test.js +132 -0
  66. package/test/shebang-strip.test.js +241 -0
  67. package/test/soul-reinject.test.js +1027 -0
  68. package/test/soul-reread.test.js +281 -0
  69. package/test/tool-output-formatter.test.js +486 -0
  70. package/test/tool-output-gating.test.js +143 -0
  71. package/test/tool-states.test.js +167 -0
  72. package/test/tools-flag.test.js +65 -0
  73. package/test/tui-attach.test.js +1255 -0
  74. package/test/tui-compaction.test.js +354 -0
  75. package/test/tui-wrap.test.js +568 -0
  76. package/test-binary.js +52 -0
  77. package/test-binary2.js +36 -0
  78. package/LICENSE +0 -21
  79. package/pave.js +0 -2
  80. package/sandbox/SandboxRunner.js +0 -1
  81. package/sandbox/pave-run.js +0 -2
  82. package/sandbox/permission.js +0 -1
  83. 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
+ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;',
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
+ }