@5ive-tech/cli 1.0.4

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 (139) hide show
  1. package/README.md +226 -0
  2. package/dist/assets/vm/five_vm_wasm.d.ts +762 -0
  3. package/dist/assets/vm/five_vm_wasm.js +3754 -0
  4. package/dist/assets/vm/five_vm_wasm_bg.wasm +0 -0
  5. package/dist/assets/vm/five_vm_wasm_bg.wasm.d.ts +247 -0
  6. package/dist/assets/vm/package.json +11 -0
  7. package/dist/cli.d.ts +47 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +343 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/analyze.d.ts +3 -0
  12. package/dist/commands/analyze.d.ts.map +1 -0
  13. package/dist/commands/analyze.js +435 -0
  14. package/dist/commands/analyze.js.map +1 -0
  15. package/dist/commands/build.d.ts +3 -0
  16. package/dist/commands/build.d.ts.map +1 -0
  17. package/dist/commands/build.js +66 -0
  18. package/dist/commands/build.js.map +1 -0
  19. package/dist/commands/compile.d.ts +3 -0
  20. package/dist/commands/compile.d.ts.map +1 -0
  21. package/dist/commands/compile.js +872 -0
  22. package/dist/commands/compile.js.map +1 -0
  23. package/dist/commands/config.d.ts +3 -0
  24. package/dist/commands/config.d.ts.map +1 -0
  25. package/dist/commands/config.js +431 -0
  26. package/dist/commands/config.js.map +1 -0
  27. package/dist/commands/deploy-and-execute.d.ts +3 -0
  28. package/dist/commands/deploy-and-execute.d.ts.map +1 -0
  29. package/dist/commands/deploy-and-execute.js +317 -0
  30. package/dist/commands/deploy-and-execute.js.map +1 -0
  31. package/dist/commands/deploy.d.ts +21 -0
  32. package/dist/commands/deploy.d.ts.map +1 -0
  33. package/dist/commands/deploy.js +806 -0
  34. package/dist/commands/deploy.js.map +1 -0
  35. package/dist/commands/donate.d.ts +4 -0
  36. package/dist/commands/donate.d.ts.map +1 -0
  37. package/dist/commands/donate.js +104 -0
  38. package/dist/commands/donate.js.map +1 -0
  39. package/dist/commands/execute.d.ts +6 -0
  40. package/dist/commands/execute.d.ts.map +1 -0
  41. package/dist/commands/execute.js +749 -0
  42. package/dist/commands/execute.js.map +1 -0
  43. package/dist/commands/fmt.d.ts +3 -0
  44. package/dist/commands/fmt.d.ts.map +1 -0
  45. package/dist/commands/fmt.js +327 -0
  46. package/dist/commands/fmt.js.map +1 -0
  47. package/dist/commands/help.d.ts +6 -0
  48. package/dist/commands/help.d.ts.map +1 -0
  49. package/dist/commands/help.js +224 -0
  50. package/dist/commands/help.js.map +1 -0
  51. package/dist/commands/index.d.ts +45 -0
  52. package/dist/commands/index.d.ts.map +1 -0
  53. package/dist/commands/index.js +119 -0
  54. package/dist/commands/index.js.map +1 -0
  55. package/dist/commands/init.d.ts +3 -0
  56. package/dist/commands/init.d.ts.map +1 -0
  57. package/dist/commands/init.js +887 -0
  58. package/dist/commands/init.js.map +1 -0
  59. package/dist/commands/local.d.ts +3 -0
  60. package/dist/commands/local.d.ts.map +1 -0
  61. package/dist/commands/local.js +703 -0
  62. package/dist/commands/local.js.map +1 -0
  63. package/dist/commands/namespace.d.ts +3 -0
  64. package/dist/commands/namespace.d.ts.map +1 -0
  65. package/dist/commands/namespace.js +328 -0
  66. package/dist/commands/namespace.js.map +1 -0
  67. package/dist/commands/template.d.ts +4 -0
  68. package/dist/commands/template.d.ts.map +1 -0
  69. package/dist/commands/template.js +486 -0
  70. package/dist/commands/template.js.map +1 -0
  71. package/dist/commands/test.d.ts +6 -0
  72. package/dist/commands/test.d.ts.map +1 -0
  73. package/dist/commands/test.js +890 -0
  74. package/dist/commands/test.js.map +1 -0
  75. package/dist/commands/version.d.ts +6 -0
  76. package/dist/commands/version.d.ts.map +1 -0
  77. package/dist/commands/version.js +339 -0
  78. package/dist/commands/version.js.map +1 -0
  79. package/dist/config/ConfigManager.d.ts +69 -0
  80. package/dist/config/ConfigManager.d.ts.map +1 -0
  81. package/dist/config/ConfigManager.js +261 -0
  82. package/dist/config/ConfigManager.js.map +1 -0
  83. package/dist/config/index.d.ts +10 -0
  84. package/dist/config/index.d.ts.map +1 -0
  85. package/dist/config/index.js +21 -0
  86. package/dist/config/index.js.map +1 -0
  87. package/dist/config/types.d.ts +35 -0
  88. package/dist/config/types.d.ts.map +1 -0
  89. package/dist/config/types.js +105 -0
  90. package/dist/config/types.js.map +1 -0
  91. package/dist/index.d.ts +3 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +29 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/project/ProjectLoader.d.ts +12 -0
  96. package/dist/project/ProjectLoader.d.ts.map +1 -0
  97. package/dist/project/ProjectLoader.js +115 -0
  98. package/dist/project/ProjectLoader.js.map +1 -0
  99. package/dist/types.d.ts +334 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/utils/AccountFixtureGenerator.d.ts +48 -0
  104. package/dist/utils/AccountFixtureGenerator.d.ts.map +1 -0
  105. package/dist/utils/AccountFixtureGenerator.js +265 -0
  106. package/dist/utils/AccountFixtureGenerator.js.map +1 -0
  107. package/dist/utils/FiveFileManager.d.ts +96 -0
  108. package/dist/utils/FiveFileManager.d.ts.map +1 -0
  109. package/dist/utils/FiveFileManager.js +329 -0
  110. package/dist/utils/FiveFileManager.js.map +1 -0
  111. package/dist/utils/ascii-art.d.ts +72 -0
  112. package/dist/utils/ascii-art.d.ts.map +1 -0
  113. package/dist/utils/ascii-art.js +314 -0
  114. package/dist/utils/ascii-art.js.map +1 -0
  115. package/dist/utils/cli-ui.d.ts +39 -0
  116. package/dist/utils/cli-ui.d.ts.map +1 -0
  117. package/dist/utils/cli-ui.js +75 -0
  118. package/dist/utils/cli-ui.js.map +1 -0
  119. package/dist/utils/fileUtils.d.ts +25 -0
  120. package/dist/utils/fileUtils.d.ts.map +1 -0
  121. package/dist/utils/fileUtils.js +50 -0
  122. package/dist/utils/fileUtils.js.map +1 -0
  123. package/dist/utils/logger.d.ts +53 -0
  124. package/dist/utils/logger.d.ts.map +1 -0
  125. package/dist/utils/logger.js +287 -0
  126. package/dist/utils/logger.js.map +1 -0
  127. package/dist/wasm/compiler.d.ts +101 -0
  128. package/dist/wasm/compiler.d.ts.map +1 -0
  129. package/dist/wasm/compiler.js +906 -0
  130. package/dist/wasm/compiler.js.map +1 -0
  131. package/dist/wasm/loader.d.ts +2 -0
  132. package/dist/wasm/loader.d.ts.map +1 -0
  133. package/dist/wasm/loader.js +90 -0
  134. package/dist/wasm/loader.js.map +1 -0
  135. package/dist/wasm/vm.d.ts +32 -0
  136. package/dist/wasm/vm.d.ts.map +1 -0
  137. package/dist/wasm/vm.js +440 -0
  138. package/dist/wasm/vm.js.map +1 -0
  139. package/package.json +100 -0
@@ -0,0 +1,890 @@
1
+ // Test command.
2
+ import { readFile, readdir, stat } from 'fs/promises';
3
+ import { join, basename, isAbsolute } from 'path';
4
+ import ora from 'ora';
5
+ import { FiveSDK, FiveTestRunner, TestDiscovery } from '@5ive-tech/sdk';
6
+ import { ConfigManager } from '../config/ConfigManager.js';
7
+ import { Connection, Keypair } from '@solana/web3.js';
8
+ import { FiveFileManager } from '../utils/FiveFileManager.js';
9
+ import { loadBuildManifest, loadProjectConfig } from '../project/ProjectLoader.js';
10
+ import { section, success as uiSuccess, error as uiError } from '../utils/cli-ui.js';
11
+ /**
12
+ * Five test command implementation
13
+ */
14
+ export const testCommand = {
15
+ name: 'test',
16
+ description: 'Run test suites',
17
+ aliases: ['t'],
18
+ options: [
19
+ {
20
+ flags: '-p, --pattern <pattern>',
21
+ description: 'Test file pattern (default: **/*.test.json)',
22
+ defaultValue: '**/*.test.json'
23
+ },
24
+ {
25
+ flags: '-f, --filter <filter>',
26
+ description: 'Run tests matching filter pattern',
27
+ required: false
28
+ },
29
+ {
30
+ flags: '--timeout <ms>',
31
+ description: 'Test timeout in milliseconds',
32
+ defaultValue: 30000
33
+ },
34
+ {
35
+ flags: '--max-cu <units>',
36
+ description: 'Maximum compute units per test',
37
+ defaultValue: 1000000
38
+ },
39
+ {
40
+ flags: '--parallel <count>',
41
+ description: 'Number of parallel test workers (0 = CPU count)',
42
+ defaultValue: 0
43
+ },
44
+ {
45
+ flags: '--benchmark',
46
+ description: 'Run performance benchmarks',
47
+ defaultValue: false
48
+ },
49
+ {
50
+ flags: '--coverage',
51
+ description: 'Generate test coverage report',
52
+ defaultValue: false
53
+ },
54
+ {
55
+ flags: '--watch',
56
+ description: 'Watch for file changes and re-run tests',
57
+ defaultValue: false
58
+ },
59
+ {
60
+ flags: '--format <format>',
61
+ description: 'Output format',
62
+ choices: ['text', 'json', 'junit'],
63
+ defaultValue: 'text'
64
+ },
65
+ {
66
+ flags: '--verbose',
67
+ description: 'Show detailed test output',
68
+ defaultValue: false
69
+ },
70
+ {
71
+ flags: '--sdk-runner',
72
+ description: 'Use modern SDK-based test runner (recommended)',
73
+ defaultValue: false
74
+ },
75
+ {
76
+ flags: '--on-chain',
77
+ description: 'Execute tests on-chain (deploy + execute)',
78
+ defaultValue: false
79
+ },
80
+ {
81
+ flags: '--batch',
82
+ description: 'Run all .bin files in batch mode',
83
+ defaultValue: false
84
+ },
85
+ {
86
+ flags: '-t, --target <target>',
87
+ description: 'Override target network (devnet, testnet, mainnet, local)',
88
+ required: false
89
+ },
90
+ {
91
+ flags: '-n, --network <url>',
92
+ description: 'Override network RPC URL',
93
+ required: false
94
+ },
95
+ {
96
+ flags: '-k, --keypair <file>',
97
+ description: 'Override keypair file path',
98
+ required: false
99
+ },
100
+ {
101
+ flags: '--retry-failed',
102
+ description: 'Retry only previously failed tests',
103
+ defaultValue: false
104
+ },
105
+ {
106
+ flags: '--analyze-costs',
107
+ description: 'Include detailed cost analysis in results',
108
+ defaultValue: false
109
+ },
110
+ {
111
+ flags: '--project <path>',
112
+ description: 'Project directory or five.toml path',
113
+ required: false
114
+ }
115
+ ],
116
+ arguments: [
117
+ {
118
+ name: 'path',
119
+ description: 'Test directory or file (default: ./tests)',
120
+ required: false
121
+ }
122
+ ],
123
+ examples: [
124
+ {
125
+ command: 'five test',
126
+ description: 'Run all tests in ./tests directory'
127
+ },
128
+ {
129
+ command: 'five test --filter "token*" --verbose',
130
+ description: 'Run token tests with verbose output'
131
+ },
132
+ {
133
+ command: 'five test ./my-tests --benchmark --format json',
134
+ description: 'Run benchmarks with JSON output'
135
+ },
136
+ {
137
+ command: 'five test --watch --parallel 4',
138
+ description: 'Watch mode with 4 parallel workers'
139
+ },
140
+ {
141
+ command: 'five test test-scripts/ --on-chain --target devnet',
142
+ description: 'Run on-chain tests on devnet'
143
+ },
144
+ {
145
+ command: 'five test test-scripts/ --on-chain --batch --analyze-costs',
146
+ description: 'Batch test all .bin files with cost analysis'
147
+ }
148
+ ],
149
+ handler: async (args, options, context) => {
150
+ const { logger } = context;
151
+ try {
152
+ const projectContext = await loadProjectConfig(options.project, process.cwd());
153
+ const manifest = projectContext ? await loadBuildManifest(projectContext.rootDir) : null;
154
+ // Apply project defaults if not provided
155
+ if (!options.target && projectContext?.config.cluster) {
156
+ options.target = projectContext.config.cluster;
157
+ }
158
+ if (!options.network && projectContext?.config.rpcUrl) {
159
+ options.network = projectContext.config.rpcUrl;
160
+ }
161
+ if (!options.keypair && projectContext?.config.keypairPath) {
162
+ options.keypair = projectContext.config.keypairPath;
163
+ }
164
+ let testPath = args[0] ||
165
+ (projectContext ? join(projectContext.rootDir, 'tests') : undefined) ||
166
+ './tests';
167
+ if (!args[0] && manifest?.artifact_path) {
168
+ testPath = isAbsolute(manifest.artifact_path)
169
+ ? manifest.artifact_path
170
+ : projectContext
171
+ ? join(projectContext.rootDir, manifest.artifact_path)
172
+ : manifest.artifact_path;
173
+ }
174
+ // Handle on-chain testing mode
175
+ if (options.onChain) {
176
+ await runOnChainTests(testPath, options, context);
177
+ return;
178
+ }
179
+ // Use modern SDK-based test runner if requested
180
+ if (options.sdkRunner) {
181
+ await runWithSdkRunner(testPath, options, context);
182
+ return;
183
+ }
184
+ // Legacy approach with SDK integration
185
+ // Initialize SDK for testing
186
+ const spinner = ora('Initializing Five SDK for testing...').start();
187
+ // No initialization needed for SDK - it's stateless
188
+ const sdk = FiveSDK.create({ debug: options.verbose });
189
+ spinner.succeed('Five SDK initialized');
190
+ // Discover test files
191
+ const testSuites = await discoverTestSuites(testPath, options, logger);
192
+ if (testSuites.length === 0) {
193
+ logger.warn('No test files found');
194
+ return;
195
+ }
196
+ logger.info(`Found ${testSuites.length} test suite(s) with ${getTotalTestCount(testSuites)} test(s)`);
197
+ // Run tests
198
+ const results = await runTestSuites(testSuites, sdk, options, context);
199
+ // Display results
200
+ displayTestResults(results, options, logger);
201
+ // Handle watch mode
202
+ if (options.watch) {
203
+ await watchAndRerun(testPath, options, context);
204
+ }
205
+ // Exit with appropriate code
206
+ const failed = results.some(suite => suite.results.some(test => !test.passed));
207
+ if (failed) {
208
+ process.exit(1);
209
+ }
210
+ }
211
+ catch (error) {
212
+ logger.error('Test execution failed:', error);
213
+ throw error;
214
+ }
215
+ }
216
+ };
217
+ /**
218
+ * Discover test suites from files (both .test.json and .v source)
219
+ */
220
+ async function discoverTestSuites(testPath, options, logger) {
221
+ const testSuites = [];
222
+ const compiledVTests = new Map();
223
+ try {
224
+ // Use new TestDiscovery to find both .test.json and .v files
225
+ const discoveredTests = await TestDiscovery.discoverTests(testPath, { verbose: options.verbose });
226
+ if (discoveredTests.length === 0 && options.verbose) {
227
+ logger.info('No tests discovered');
228
+ }
229
+ // Organize discovered tests into suites
230
+ const suiteMap = new Map();
231
+ for (const test of discoveredTests) {
232
+ if (test.type === 'v-source') {
233
+ // Compile .v source file if not already compiled
234
+ if (!compiledVTests.has(test.path)) {
235
+ const spinner = ora(`Compiling ${basename(test.path)}...`).start();
236
+ try {
237
+ const compilation = await TestDiscovery.compileVTest(test.path);
238
+ if (compilation.success && compilation.bytecode) {
239
+ compiledVTests.set(test.path, compilation.bytecode);
240
+ spinner.succeed(`Compiled ${basename(test.path)}`);
241
+ }
242
+ else {
243
+ spinner.fail(`Failed to compile ${basename(test.path)}`);
244
+ logger.error(`Compilation errors: ${compilation.errors?.join(', ')}`);
245
+ continue;
246
+ }
247
+ }
248
+ catch (error) {
249
+ spinner.fail(`Error compiling ${basename(test.path)}`);
250
+ logger.error(error instanceof Error ? error.message : 'Unknown error');
251
+ continue;
252
+ }
253
+ }
254
+ // Create test case from compiled .v file
255
+ const bytecode = compiledVTests.get(test.path);
256
+ if (bytecode) {
257
+ // Write bytecode to temp location
258
+ const fs = await import('fs/promises');
259
+ const tmpDir = join(process.cwd(), '.five', 'test-cache');
260
+ await fs.mkdir(tmpDir, { recursive: true });
261
+ const bytecodeFile = join(tmpDir, `${test.name.replace(/:/g, '_')}.bin`);
262
+ await fs.writeFile(bytecodeFile, bytecode);
263
+ const suite = suiteMap.get(test.path) || [];
264
+ suite.push({
265
+ name: test.name,
266
+ bytecode: bytecodeFile,
267
+ input: undefined,
268
+ accounts: undefined,
269
+ expected: {
270
+ success: true,
271
+ result: test.parameters ? test.parameters[test.parameters.length - 1] : undefined
272
+ }
273
+ });
274
+ suiteMap.set(test.path, suite);
275
+ }
276
+ }
277
+ else if (test.type === 'json-suite') {
278
+ // Load .test.json suite
279
+ const suite = await loadTestSuite(test.path);
280
+ if (suite) {
281
+ testSuites.push(suite);
282
+ }
283
+ }
284
+ }
285
+ // Convert compiled V tests to TestSuite format
286
+ for (const [filePath, testCases] of suiteMap.entries()) {
287
+ testSuites.push({
288
+ name: `${basename(filePath, '.v')}`,
289
+ description: `Tests from ${filePath}`,
290
+ testCases
291
+ });
292
+ }
293
+ }
294
+ catch (error) {
295
+ logger.warn(`Failed to discover tests at ${testPath}: ${error}`);
296
+ }
297
+ // Apply filter if specified
298
+ if (options.filter) {
299
+ return testSuites.filter(suite => suite.name.includes(options.filter) ||
300
+ suite.testCases.some(test => test.name.includes(options.filter)));
301
+ }
302
+ return testSuites;
303
+ }
304
+ /**
305
+ * Load test suite from JSON file
306
+ */
307
+ async function loadTestSuite(filePath) {
308
+ try {
309
+ const content = await readFile(filePath, 'utf8');
310
+ const data = JSON.parse(content);
311
+ return {
312
+ name: data.name || basename(filePath, '.test.json'),
313
+ description: data.description,
314
+ testCases: data.tests || data.testCases || []
315
+ };
316
+ }
317
+ catch (error) {
318
+ console.warn(`Failed to load test suite ${filePath}: ${error}`);
319
+ return null;
320
+ }
321
+ }
322
+ /**
323
+ * Run all test suites
324
+ */
325
+ async function runTestSuites(testSuites, sdk, options, context) {
326
+ const { logger } = context;
327
+ const results = [];
328
+ for (const suite of testSuites) {
329
+ logger.info(`Running test suite: ${suite.name}`);
330
+ const suiteResults = [];
331
+ for (const testCase of suite.testCases) {
332
+ const result = await runSingleTest(testCase, sdk, options, context);
333
+ suiteResults.push(result);
334
+ if (options.verbose) {
335
+ displaySingleTestResult(result, logger);
336
+ }
337
+ }
338
+ results.push({ suite, results: suiteResults });
339
+ }
340
+ return results;
341
+ }
342
+ /**
343
+ * Run a single test case
344
+ */
345
+ async function runSingleTest(testCase, sdk, options, context) {
346
+ const { logger } = context;
347
+ const startTime = Date.now();
348
+ try {
349
+ // Load bytecode using centralized manager
350
+ const fileManager = FiveFileManager.getInstance();
351
+ const loadedFile = await fileManager.loadFile(testCase.bytecode, {
352
+ validateFormat: true
353
+ });
354
+ const bytecode = loadedFile.bytecode;
355
+ // Validation already done by file manager with validateFormat: true
356
+ const validation = { valid: true }; // Skip redundant validation
357
+ // Validation handled by centralized file manager
358
+ // Parse input parameters if specified
359
+ let parameters = [];
360
+ if (testCase.input) {
361
+ const inputData = await readFile(testCase.input, 'utf8');
362
+ try {
363
+ parameters = JSON.parse(inputData);
364
+ }
365
+ catch {
366
+ // If not JSON, treat as raw string parameter
367
+ parameters = [inputData];
368
+ }
369
+ }
370
+ // Execute with timeout using Five SDK
371
+ const executionPromise = FiveSDK.executeLocally(bytecode, 0, // Default to first function
372
+ parameters, {
373
+ debug: options.verbose,
374
+ trace: options.verbose,
375
+ computeUnitLimit: options.maxCu,
376
+ abi: loadedFile.abi // Pass ABI for function name resolution
377
+ });
378
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), options.timeout));
379
+ const result = await Promise.race([executionPromise, timeoutPromise]);
380
+ const duration = Date.now() - startTime;
381
+ // Validate result against expected
382
+ const passed = validateTestResult(result, testCase.expected);
383
+ return {
384
+ name: testCase.name,
385
+ passed,
386
+ duration,
387
+ computeUnits: result.computeUnitsUsed || 0,
388
+ details: options.verbose ? result : undefined
389
+ };
390
+ }
391
+ catch (error) {
392
+ const duration = Date.now() - startTime;
393
+ // Check if error was expected
394
+ const passed = testCase.expected.success === false &&
395
+ testCase.expected.error !== undefined &&
396
+ error instanceof Error &&
397
+ error.message.includes(testCase.expected.error);
398
+ return {
399
+ name: testCase.name,
400
+ passed,
401
+ duration,
402
+ error: error instanceof Error ? error.message : 'Unknown error'
403
+ };
404
+ }
405
+ }
406
+ /**
407
+ * Validate test result against expected outcome
408
+ */
409
+ function validateTestResult(result, expected) {
410
+ // Check success/failure
411
+ if (result.success !== expected.success) {
412
+ return false;
413
+ }
414
+ // If expecting success, check result value
415
+ if (expected.success && expected.result !== undefined) {
416
+ if (JSON.stringify(result.result) !== JSON.stringify(expected.result)) {
417
+ return false;
418
+ }
419
+ }
420
+ // Check compute units limit
421
+ if (expected.maxComputeUnits && result.computeUnitsUsed > expected.maxComputeUnits) {
422
+ return false;
423
+ }
424
+ return true;
425
+ }
426
+ /**
427
+ * Display single test result
428
+ */
429
+ function displaySingleTestResult(result, logger) {
430
+ const status = result.passed ? 'OK' : 'FAIL';
431
+ const duration = `(${result.duration}ms)`;
432
+ const cu = result.computeUnits ? `[${result.computeUnits} CU]` : '';
433
+ console.log(` ${status} ${result.name} ${duration} ${cu}`);
434
+ if (!result.passed && result.error) {
435
+ console.log(` Error: ${result.error}`);
436
+ }
437
+ }
438
+ /**
439
+ * Display comprehensive test results
440
+ */
441
+ function displayTestResults(results, options, logger) {
442
+ if (options.format === 'json') {
443
+ console.log(JSON.stringify(results, null, 2));
444
+ return;
445
+ }
446
+ console.log('\n' + section('Test Results'));
447
+ let totalTests = 0;
448
+ let totalPassed = 0;
449
+ let totalDuration = 0;
450
+ for (const { suite, results: suiteResults } of results) {
451
+ const passed = suiteResults.filter(r => r.passed).length;
452
+ const total = suiteResults.length;
453
+ const suiteDuration = suiteResults.reduce((sum, r) => sum + r.duration, 0);
454
+ totalTests += total;
455
+ totalPassed += passed;
456
+ totalDuration += suiteDuration;
457
+ const status = passed === total ? 'OK' : 'FAIL';
458
+ console.log(`\n${status} ${suite.name}: ${passed}/${total} passed (${suiteDuration}ms)`);
459
+ if (options.verbose || passed !== total) {
460
+ suiteResults.forEach(result => displaySingleTestResult(result, logger));
461
+ }
462
+ }
463
+ // Summary
464
+ console.log('\n' + section('Summary'));
465
+ console.log(` Total: ${totalTests} tests`);
466
+ console.log(` Passed: ${totalPassed}`);
467
+ const failed = totalTests - totalPassed;
468
+ if (failed > 0) {
469
+ console.log(` Failed: ${failed}`);
470
+ }
471
+ console.log(` Duration: ${totalDuration}ms`);
472
+ if (failed === 0) {
473
+ console.log(uiSuccess('All tests passed'));
474
+ }
475
+ else {
476
+ console.log(uiError(`${failed} test(s) failed`));
477
+ }
478
+ }
479
+ /**
480
+ * Watch for file changes and re-run tests
481
+ */
482
+ async function watchAndRerun(testPath, options, context) {
483
+ const { logger } = context;
484
+ // Dynamic import for file watching
485
+ const chokidar = await import('chokidar');
486
+ logger.info('Watching for file changes...');
487
+ const watcher = chokidar.watch([testPath, '**/*.bin'], {
488
+ persistent: true,
489
+ ignoreInitial: true
490
+ });
491
+ watcher.on('change', async (filePath) => {
492
+ logger.info(`File changed: ${filePath}`);
493
+ logger.info('Re-running tests...');
494
+ try {
495
+ // Re-run the test command
496
+ const testSuites = await discoverTestSuites(testPath, options, logger);
497
+ const sdk = FiveSDK.create({ debug: options.verbose });
498
+ const results = await runTestSuites(testSuites, sdk, options, context);
499
+ displayTestResults(results, options, logger);
500
+ }
501
+ catch (error) {
502
+ logger.error(`Test re-run failed: ${error}`);
503
+ }
504
+ });
505
+ // Handle graceful shutdown
506
+ process.on('SIGINT', () => {
507
+ logger.info('Stopping test watcher...');
508
+ watcher.close();
509
+ process.exit(0);
510
+ });
511
+ }
512
+ /**
513
+ * Run on-chain tests with deploy + execute pipeline
514
+ */
515
+ async function runOnChainTests(testPath, options, context) {
516
+ const { logger } = context;
517
+ logger.info('Starting on-chain test pipeline');
518
+ try {
519
+ // Apply configuration with CLI overrides
520
+ const configManager = ConfigManager.getInstance();
521
+ const overrides = {
522
+ target: options.target,
523
+ network: options.network,
524
+ keypair: options.keypair
525
+ };
526
+ const config = await configManager.applyOverrides(overrides);
527
+ const targetPrefix = ConfigManager.getTargetPrefix(config.target);
528
+ logger.info(`${targetPrefix} Testing on ${config.target}`);
529
+ logger.info(`Network: ${config.networks[config.target].rpcUrl}`);
530
+ logger.info(`Keypair: ${config.keypairPath}`);
531
+ // Discover .bin test files
532
+ const testFiles = await discoverBinFiles(testPath, options);
533
+ if (testFiles.length === 0) {
534
+ logger.warn('No .bin test files found');
535
+ return;
536
+ }
537
+ logger.info(`Found ${testFiles.length} test script(s)`);
538
+ // Setup Solana connection and keypair
539
+ const connection = new Connection(config.networks[config.target].rpcUrl, 'confirmed');
540
+ const signerKeypair = await loadKeypair(config.keypairPath);
541
+ logger.info(`Deployer: ${signerKeypair.publicKey.toString()}`);
542
+ // Run batch testing
543
+ const results = await runBatchOnChainTests(testFiles, connection, signerKeypair, options, config);
544
+ // Display comprehensive results
545
+ displayOnChainTestResults(results, options, logger);
546
+ // Exit with appropriate code
547
+ if (results.failed > 0) {
548
+ logger.error(`${results.failed}/${results.totalScripts} tests failed`);
549
+ process.exit(1);
550
+ }
551
+ else {
552
+ logger.info(`All ${results.passed}/${results.totalScripts} tests passed`);
553
+ }
554
+ }
555
+ catch (error) {
556
+ logger.error('On-chain testing failed:', error);
557
+ throw error;
558
+ }
559
+ }
560
+ /**
561
+ * Run tests using modern SDK-based test runner
562
+ */
563
+ async function runWithSdkRunner(testPath, options, context) {
564
+ const { logger } = context;
565
+ logger.info('Using Five SDK test runner');
566
+ // Create test runner with options
567
+ const runner = new FiveTestRunner({
568
+ timeout: options.timeout,
569
+ maxComputeUnits: options.maxCu,
570
+ parallel: options.parallel || 0,
571
+ verbose: options.verbose,
572
+ debug: options.verbose,
573
+ trace: options.verbose,
574
+ pattern: options.filter || '*',
575
+ failFast: false
576
+ });
577
+ try {
578
+ // Discover test suites
579
+ const testSuites = await runner.discoverTestSuites(testPath);
580
+ if (testSuites.length === 0) {
581
+ logger.warn('No test files found');
582
+ return;
583
+ }
584
+ logger.info(`Found ${testSuites.length} test suite(s)`);
585
+ // Run test suites
586
+ const results = await runner.runTestSuites(testSuites);
587
+ // Display results in requested format
588
+ if (options.format === 'json') {
589
+ console.log(JSON.stringify(results, null, 2));
590
+ }
591
+ else {
592
+ displaySdkTestResults(results, logger);
593
+ }
594
+ // Check for failures
595
+ const totalFailed = results.reduce((sum, r) => sum + r.failed, 0);
596
+ if (totalFailed > 0) {
597
+ process.exit(1);
598
+ }
599
+ }
600
+ catch (error) {
601
+ logger.error('SDK Test Runner failed:', error);
602
+ process.exit(1);
603
+ }
604
+ }
605
+ /**
606
+ * Display SDK test results
607
+ */
608
+ function displaySdkTestResults(results, logger) {
609
+ logger.info('\nTest Results Summary:');
610
+ let totalPassed = 0;
611
+ let totalFailed = 0;
612
+ let totalSkipped = 0;
613
+ let totalDuration = 0;
614
+ for (const result of results) {
615
+ totalPassed += result.passed;
616
+ totalFailed += result.failed;
617
+ totalSkipped += result.skipped;
618
+ totalDuration += result.duration;
619
+ const status = result.failed === 0 ? 'OK' : 'FAIL';
620
+ logger.info(`${status} ${result.suite.name}: ${result.passed}/${result.passed + result.failed + result.skipped} passed (${result.duration}ms)`);
621
+ if (result.failed > 0) {
622
+ const failedTests = result.results.filter((r) => !r.passed);
623
+ for (const test of failedTests) {
624
+ logger.error(` FAIL ${test.name}: ${test.error || 'Test failed'}`);
625
+ }
626
+ }
627
+ }
628
+ logger.info(`\nOverall: ${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped (${totalDuration}ms)`);
629
+ if (totalFailed === 0) {
630
+ logger.info(uiSuccess('All tests passed'));
631
+ }
632
+ else {
633
+ logger.error(uiError(`${totalFailed} test(s) failed`));
634
+ }
635
+ }
636
+ /**
637
+ * Get total test count across all suites
638
+ */
639
+ function getTotalTestCount(testSuites) {
640
+ return testSuites.reduce((total, suite) => total + suite.testCases.length, 0);
641
+ }
642
+ /**
643
+ * Discover .bin files for on-chain testing
644
+ */
645
+ async function discoverBinFiles(testPath, options) {
646
+ const binFiles = [];
647
+ try {
648
+ const stats = await stat(testPath);
649
+ if (stats.isFile()) {
650
+ // Single file - check if it's a supported artifact
651
+ if (testPath.endsWith('.bin') || testPath.endsWith('.five')) {
652
+ binFiles.push(testPath);
653
+ }
654
+ }
655
+ else if (stats.isDirectory()) {
656
+ // Directory - recursively find all artifact files
657
+ const files = await readdir(testPath, { recursive: true });
658
+ for (const file of files) {
659
+ if (typeof file === 'string' &&
660
+ (file.endsWith('.bin') || file.endsWith('.five'))) {
661
+ const fullPath = join(testPath, file);
662
+ // Skip node_modules directories
663
+ if (fullPath.includes('node_modules')) {
664
+ continue;
665
+ }
666
+ try {
667
+ // Verify it's actually a file, not a directory
668
+ const fileStats = await stat(fullPath);
669
+ if (fileStats.isFile()) {
670
+ binFiles.push(fullPath);
671
+ }
672
+ }
673
+ catch (error) {
674
+ // Skip files that can't be accessed
675
+ continue;
676
+ }
677
+ }
678
+ }
679
+ }
680
+ }
681
+ catch (error) {
682
+ console.warn(`Failed to discover .bin files at ${testPath}: ${error}`);
683
+ }
684
+ // Apply filter if specified
685
+ if (options.filter) {
686
+ return binFiles.filter(file => basename(file).includes(options.filter));
687
+ }
688
+ return binFiles.sort(); // Sort for consistent ordering
689
+ }
690
+ /**
691
+ * Load keypair from file path
692
+ */
693
+ async function loadKeypair(keypairPath) {
694
+ try {
695
+ const keypairData = await readFile(keypairPath, 'utf8');
696
+ const secretKey = JSON.parse(keypairData);
697
+ return Keypair.fromSecretKey(new Uint8Array(secretKey));
698
+ }
699
+ catch (error) {
700
+ throw new Error(`Failed to load keypair from ${keypairPath}: ${error}`);
701
+ }
702
+ }
703
+ /**
704
+ * Run batch on-chain tests with deploy → execute → verify pipeline
705
+ */
706
+ async function runBatchOnChainTests(testFiles, connection, signerKeypair, options, config) {
707
+ const results = [];
708
+ const startTime = Date.now();
709
+ let totalCost = 0;
710
+ console.log('\n' + section(`Running ${testFiles.length} On-Chain Tests`));
711
+ for (let i = 0; i < testFiles.length; i++) {
712
+ const scriptFile = testFiles[i];
713
+ const scriptName = basename(scriptFile, '.bin');
714
+ const testStartTime = Date.now();
715
+ const spinner = ora(`[${i + 1}/${testFiles.length}] Testing ${scriptName}...`).start();
716
+ try {
717
+ // Load bytecode using centralized manager
718
+ const fileManager = FiveFileManager.getInstance();
719
+ const loadedFile = await fileManager.loadFile(scriptFile, {
720
+ validateFormat: true
721
+ });
722
+ const bytecode = loadedFile.bytecode;
723
+ if (options.verbose || options.debug) {
724
+ spinner.text = `[${i + 1}/${testFiles.length}] Deploying ${scriptName} (${bytecode.length} bytes)...`;
725
+ }
726
+ // Deploy script
727
+ const deployResult = await FiveSDK.deployToSolana(bytecode, connection, signerKeypair, {
728
+ debug: options.verbose || options.debug || false,
729
+ network: config.target,
730
+ computeBudget: 1000000,
731
+ maxRetries: 3
732
+ });
733
+ if (!deployResult.success) {
734
+ spinner.fail(`[${i + 1}/${testFiles.length}] ${scriptName} deployment failed`);
735
+ results.push({
736
+ scriptFile,
737
+ passed: false,
738
+ deployResult: {
739
+ success: false,
740
+ error: deployResult.error,
741
+ cost: deployResult.deploymentCost || 0
742
+ },
743
+ totalDuration: Date.now() - testStartTime,
744
+ totalCost: deployResult.deploymentCost || 0,
745
+ error: `Deployment failed: ${deployResult.error}`
746
+ });
747
+ totalCost += deployResult.deploymentCost || 0;
748
+ continue;
749
+ }
750
+ if (options.verbose || options.debug) {
751
+ spinner.text = `[${i + 1}/${testFiles.length}] Executing ${scriptName}...`;
752
+ }
753
+ // Execute script (function 0 with no parameters)
754
+ const executeResult = await FiveSDK.executeOnSolana(deployResult.programId, connection, signerKeypair, 0, // Function index 0
755
+ [], // No parameters
756
+ [], // No additional accounts
757
+ {
758
+ debug: options.verbose || options.debug || false,
759
+ network: config.target,
760
+ computeUnitLimit: 1000000,
761
+ maxRetries: 3
762
+ });
763
+ const testDuration = Date.now() - testStartTime;
764
+ const testCost = (deployResult.deploymentCost || 0) + (executeResult.cost || 0);
765
+ totalCost += testCost;
766
+ const passed = deployResult.success && executeResult.success;
767
+ if (passed) {
768
+ spinner.succeed(`[${i + 1}/${testFiles.length}] ${scriptName} OK (${testDuration}ms, ${(testCost / 1e9).toFixed(4)} SOL)`);
769
+ }
770
+ else {
771
+ spinner.fail(`[${i + 1}/${testFiles.length}] ${scriptName} FAIL (${testDuration}ms)`);
772
+ }
773
+ results.push({
774
+ scriptFile,
775
+ passed,
776
+ deployResult: {
777
+ success: deployResult.success,
778
+ scriptAccount: deployResult.programId,
779
+ transactionId: deployResult.transactionId,
780
+ cost: deployResult.deploymentCost || 0,
781
+ error: deployResult.error
782
+ },
783
+ executeResult: {
784
+ success: executeResult.success,
785
+ transactionId: executeResult.transactionId,
786
+ computeUnitsUsed: executeResult.computeUnitsUsed,
787
+ result: executeResult.result,
788
+ error: executeResult.error
789
+ },
790
+ totalDuration: testDuration,
791
+ totalCost: testCost
792
+ });
793
+ }
794
+ catch (error) {
795
+ const testDuration = Date.now() - testStartTime;
796
+ spinner.fail(`[${i + 1}/${testFiles.length}] ${scriptName} FAIL (error)`);
797
+ results.push({
798
+ scriptFile,
799
+ passed: false,
800
+ totalDuration: testDuration,
801
+ totalCost: 0,
802
+ error: error instanceof Error ? error.message : 'Unknown error'
803
+ });
804
+ }
805
+ }
806
+ const totalDuration = Date.now() - startTime;
807
+ const passed = results.filter(r => r.passed).length;
808
+ const failed = results.length - passed;
809
+ return {
810
+ totalScripts: testFiles.length,
811
+ passed,
812
+ failed,
813
+ totalCost,
814
+ totalDuration,
815
+ results
816
+ };
817
+ }
818
+ /**
819
+ * Display comprehensive on-chain test results
820
+ */
821
+ function displayOnChainTestResults(summary, options, logger) {
822
+ console.log('\n' + section('On-Chain Test Results'));
823
+ // Overall statistics
824
+ const successRate = ((summary.passed / summary.totalScripts) * 100).toFixed(1);
825
+ const avgCostPerScript = summary.totalCost / summary.totalScripts;
826
+ const totalCostSOL = summary.totalCost / 1e9;
827
+ console.log(`Passed: ${summary.passed}/${summary.totalScripts} (${successRate}%)`);
828
+ console.log(`Total duration: ${summary.totalDuration}ms`);
829
+ console.log(`Total cost: ${totalCostSOL.toFixed(6)} SOL`);
830
+ console.log(`Average cost per script: ${(avgCostPerScript / 1e9).toFixed(6)} SOL`);
831
+ if (options.analyzeCosts) {
832
+ console.log('\n' + section('Cost Analysis'));
833
+ let deploymentCost = 0;
834
+ let executionCost = 0;
835
+ for (const result of summary.results) {
836
+ if (result.deployResult?.cost) {
837
+ deploymentCost += result.deployResult.cost;
838
+ }
839
+ if (result.executeResult?.cost) {
840
+ executionCost += result.executeResult.cost;
841
+ }
842
+ }
843
+ console.log(`Total deployment cost: ${(deploymentCost / 1e9).toFixed(6)} SOL`);
844
+ console.log(`Total execution cost: ${(executionCost / 1e9).toFixed(6)} SOL`);
845
+ console.log(`Deployment vs Execution: ${((deploymentCost / summary.totalCost) * 100).toFixed(1)}% : ${((executionCost / summary.totalCost) * 100).toFixed(1)}%`);
846
+ }
847
+ // Failed tests details
848
+ if (summary.failed > 0) {
849
+ console.log('\n' + section('Failed Tests'));
850
+ const failedResults = summary.results.filter(r => !r.passed);
851
+ for (const result of failedResults) {
852
+ const scriptName = basename(result.scriptFile, '.bin');
853
+ console.log(` FAIL ${scriptName}:`);
854
+ if (result.error) {
855
+ console.log(` Error: ${result.error}`);
856
+ }
857
+ if (result.deployResult && !result.deployResult.success) {
858
+ console.log(` Deployment: Failed - ${result.deployResult.error}`);
859
+ }
860
+ if (result.executeResult && !result.executeResult.success) {
861
+ console.log(` Execution: Failed - ${result.executeResult.error}`);
862
+ }
863
+ console.log(` Duration: ${result.totalDuration}ms`);
864
+ console.log(` Cost: ${(result.totalCost / 1e9).toFixed(6)} SOL\n`);
865
+ }
866
+ }
867
+ // Successful tests (if verbose)
868
+ if (options.verbose && summary.passed > 0) {
869
+ console.log('\n' + section('Successful Tests'));
870
+ const passedResults = summary.results.filter(r => r.passed);
871
+ for (const result of passedResults) {
872
+ const scriptName = basename(result.scriptFile, '.bin');
873
+ const deployTx = result.deployResult?.transactionId?.substring(0, 8) || 'N/A';
874
+ const executeTx = result.executeResult?.transactionId?.substring(0, 8) || 'N/A';
875
+ const computeUnits = result.executeResult?.computeUnitsUsed || 0;
876
+ console.log(` OK ${scriptName}:`);
877
+ console.log(` Deploy: ${deployTx}... | Execute: ${executeTx}...`);
878
+ console.log(` Compute Units: ${computeUnits.toLocaleString()}`);
879
+ console.log(` Duration: ${result.totalDuration}ms | Cost: ${(result.totalCost / 1e9).toFixed(6)} SOL\n`);
880
+ }
881
+ }
882
+ // Summary message
883
+ if (summary.failed === 0) {
884
+ console.log(uiSuccess('All on-chain tests passed'));
885
+ }
886
+ else {
887
+ console.log(uiError(`${summary.failed} test(s) failed. Check logs above for details.`));
888
+ }
889
+ }
890
+ //# sourceMappingURL=test.js.map