@5ive-tech/cli 1.0.22 → 1.0.24

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.
@@ -1,12 +1,12 @@
1
1
  // Test command.
2
2
  import { readFile, readdir, stat } from 'fs/promises';
3
- import { join, basename, isAbsolute } from 'path';
3
+ import { join, basename } from 'path';
4
4
  import ora from 'ora';
5
5
  import { FiveSDK, FiveTestRunner, TestDiscovery } from '@5ive-tech/sdk';
6
6
  import { ConfigManager } from '../config/ConfigManager.js';
7
7
  import { Connection, Keypair } from '@solana/web3.js';
8
8
  import { FiveFileManager } from '../utils/FiveFileManager.js';
9
- import { loadBuildManifest, loadProjectConfig } from '../project/ProjectLoader.js';
9
+ import { loadProjectConfig } from '../project/ProjectLoader.js';
10
10
  import { section, success as uiSuccess, error as uiError } from '../utils/cli-ui.js';
11
11
  /**
12
12
  * 5IVE test command implementation
@@ -18,8 +18,8 @@ export const testCommand = {
18
18
  options: [
19
19
  {
20
20
  flags: '-p, --pattern <pattern>',
21
- description: 'Test file pattern (default: **/*.test.json)',
22
- defaultValue: '**/*.test.json'
21
+ description: 'Test discovery pattern (default: *)',
22
+ defaultValue: '*'
23
23
  },
24
24
  {
25
25
  flags: '-f, --filter <filter>',
@@ -107,6 +107,16 @@ export const testCommand = {
107
107
  description: 'Include detailed cost analysis in results',
108
108
  defaultValue: false
109
109
  },
110
+ {
111
+ flags: '--allow-mainnet-tests',
112
+ description: 'Allow on-chain tests on mainnet (requires --max-cost-sol)',
113
+ defaultValue: false
114
+ },
115
+ {
116
+ flags: '--max-cost-sol <amount>',
117
+ description: 'Maximum SOL budget for on-chain test runs',
118
+ required: false
119
+ },
110
120
  {
111
121
  flags: '--project <path>',
112
122
  description: 'Project directory or five.toml path',
@@ -150,7 +160,6 @@ export const testCommand = {
150
160
  const { logger } = context;
151
161
  try {
152
162
  const projectContext = await loadProjectConfig(options.project, process.cwd());
153
- const manifest = projectContext ? await loadBuildManifest(projectContext.rootDir) : null;
154
163
  // Apply project defaults if not provided
155
164
  if (!options.target && projectContext?.config.cluster) {
156
165
  options.target = projectContext.config.cluster;
@@ -161,16 +170,9 @@ export const testCommand = {
161
170
  if (!options.keypair && projectContext?.config.keypairPath) {
162
171
  options.keypair = projectContext.config.keypairPath;
163
172
  }
164
- let testPath = args[0] ||
173
+ const testPath = args[0] ||
165
174
  (projectContext ? join(projectContext.rootDir, 'tests') : undefined) ||
166
175
  './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
176
  // Handle on-chain testing mode
175
177
  if (options.onChain) {
176
178
  await runOnChainTests(testPath, options, context);
@@ -220,9 +222,16 @@ export const testCommand = {
220
222
  async function discoverTestSuites(testPath, options, logger) {
221
223
  const testSuites = [];
222
224
  const compiledVTests = new Map();
225
+ const loadedJsonSuites = new Set();
223
226
  try {
224
227
  // Use new TestDiscovery to find both .test.json and .v files
225
228
  const discoveredTests = await TestDiscovery.discoverTests(testPath, { verbose: options.verbose });
229
+ const discoveredVTests = discoveredTests.filter((t) => t.type === 'v-source' && t.source);
230
+ // Fallback for SDK versions that do not yet discover pub test_* .v tests.
231
+ if (discoveredVTests.length === 0) {
232
+ const fallbackVTests = await discoverVTestsLocally(testPath);
233
+ discoveredTests.push(...fallbackVTests);
234
+ }
226
235
  if (discoveredTests.length === 0 && options.verbose) {
227
236
  logger.info('No tests discovered');
228
237
  }
@@ -234,9 +243,13 @@ async function discoverTestSuites(testPath, options, logger) {
234
243
  if (!compiledVTests.has(test.path)) {
235
244
  const spinner = ora(`Compiling ${basename(test.path)}...`).start();
236
245
  try {
237
- const compilation = await TestDiscovery.compileVTest(test.path);
246
+ const source = await readFile(test.path, 'utf8');
247
+ const compilation = await FiveSDK.compile({ filename: test.path, content: source }, { debug: options.verbose, optimize: false });
238
248
  if (compilation.success && compilation.bytecode) {
239
- compiledVTests.set(test.path, compilation.bytecode);
249
+ compiledVTests.set(test.path, {
250
+ bytecode: compilation.bytecode,
251
+ abi: compilation.abi
252
+ });
240
253
  spinner.succeed(`Compiled ${basename(test.path)}`);
241
254
  }
242
255
  else {
@@ -252,33 +265,35 @@ async function discoverTestSuites(testPath, options, logger) {
252
265
  }
253
266
  }
254
267
  // 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);
268
+ const compiled = compiledVTests.get(test.path);
269
+ if (compiled && test.source) {
270
+ const sourceMeta = test.source;
263
271
  const suite = suiteMap.get(test.path) || [];
264
272
  suite.push({
265
273
  name: test.name,
266
- bytecode: bytecodeFile,
267
- input: undefined,
268
- accounts: undefined,
274
+ bytecode: test.path,
275
+ sourceFile: test.path,
276
+ functionRef: test.source.functionName,
277
+ parameters: test.parameters || [],
278
+ inlineBytecode: compiled.bytecode,
279
+ inlineAbi: compiled.abi,
269
280
  expected: {
270
281
  success: true,
271
- result: test.parameters ? test.parameters[test.parameters.length - 1] : undefined
282
+ result: sourceMeta?.expectsResult ? sourceMeta?.expectedResult : undefined
272
283
  }
273
284
  });
274
285
  suiteMap.set(test.path, suite);
275
286
  }
276
287
  }
277
288
  else if (test.type === 'json-suite') {
289
+ if (loadedJsonSuites.has(test.path)) {
290
+ continue;
291
+ }
278
292
  // Load .test.json suite
279
293
  const suite = await loadTestSuite(test.path);
280
294
  if (suite) {
281
295
  testSuites.push(suite);
296
+ loadedJsonSuites.add(test.path);
282
297
  }
283
298
  }
284
299
  }
@@ -308,10 +323,15 @@ async function loadTestSuite(filePath) {
308
323
  try {
309
324
  const content = await readFile(filePath, 'utf8');
310
325
  const data = JSON.parse(content);
326
+ const testCases = data.tests || data.testCases || [];
327
+ // Ignore fixture-shaped JSON files in local test mode.
328
+ if (!Array.isArray(testCases)) {
329
+ return null;
330
+ }
311
331
  return {
312
332
  name: data.name || basename(filePath, '.test.json'),
313
333
  description: data.description,
314
- testCases: data.tests || data.testCases || []
334
+ testCases
315
335
  };
316
336
  }
317
337
  catch (error) {
@@ -319,6 +339,101 @@ async function loadTestSuite(filePath) {
319
339
  return null;
320
340
  }
321
341
  }
342
+ async function discoverVTestsLocally(testPath) {
343
+ const vFiles = await findFilesByExtension(testPath, '.v');
344
+ const tests = [];
345
+ for (const file of vFiles) {
346
+ const content = await readFile(file, 'utf8');
347
+ const lines = content.split('\n');
348
+ let pendingParams;
349
+ for (const rawLine of lines) {
350
+ const line = rawLine.trim();
351
+ const paramsMatch = line.match(/@test-params(?:\s+(.*))?$/);
352
+ if (paramsMatch) {
353
+ const paramsStr = (paramsMatch[1] || '').trim();
354
+ if (paramsStr.length === 0) {
355
+ pendingParams = [];
356
+ }
357
+ else if (paramsStr.startsWith('[')) {
358
+ const parsed = JSON.parse(paramsStr);
359
+ pendingParams = Array.isArray(parsed) ? parsed : [];
360
+ }
361
+ else {
362
+ pendingParams = paramsStr
363
+ .split(/\s+/)
364
+ .filter(Boolean)
365
+ .map(parseTestParamToken);
366
+ }
367
+ continue;
368
+ }
369
+ const funcMatch = line.match(/^pub\s+(?:fn\s+)?(test_[A-Za-z0-9_]*|[A-Za-z0-9_]*_test)\s*\([^)]*\)\s*(?:->\s*([A-Za-z0-9_<>\[\]]+))?/);
370
+ if (!funcMatch)
371
+ continue;
372
+ const functionName = funcMatch[1];
373
+ const hasReturnValue = !!funcMatch[2];
374
+ const [parameters, expectedResult, expectsResult] = splitParamsAndExpectation(pendingParams, hasReturnValue);
375
+ tests.push({
376
+ name: `${basename(file, '.v')}::${functionName}`,
377
+ path: file,
378
+ type: 'v-source',
379
+ source: {
380
+ functionName,
381
+ expectedResult,
382
+ expectsResult
383
+ },
384
+ parameters: parameters.length > 0 ? parameters : undefined
385
+ });
386
+ pendingParams = undefined;
387
+ }
388
+ }
389
+ return tests;
390
+ }
391
+ async function findFilesByExtension(testPath, extension) {
392
+ const result = [];
393
+ const stats = await stat(testPath);
394
+ if (stats.isFile()) {
395
+ if (testPath.endsWith(extension)) {
396
+ result.push(testPath);
397
+ }
398
+ return result;
399
+ }
400
+ const entries = await readdir(testPath, { recursive: true });
401
+ for (const entry of entries) {
402
+ if (typeof entry !== 'string')
403
+ continue;
404
+ if (!entry.endsWith(extension))
405
+ continue;
406
+ const fullPath = join(testPath, entry);
407
+ if (fullPath.includes('node_modules'))
408
+ continue;
409
+ const entryStats = await stat(fullPath);
410
+ if (entryStats.isFile()) {
411
+ result.push(fullPath);
412
+ }
413
+ }
414
+ return result;
415
+ }
416
+ function splitParamsAndExpectation(values, hasReturnValue) {
417
+ const parsed = Array.isArray(values) ? values : [];
418
+ if (!hasReturnValue || parsed.length === 0) {
419
+ return [parsed, undefined, false];
420
+ }
421
+ return [parsed.slice(0, parsed.length - 1), parsed[parsed.length - 1], true];
422
+ }
423
+ function parseTestParamToken(token) {
424
+ if ((token.startsWith('"') && token.endsWith('"')) ||
425
+ (token.startsWith("'") && token.endsWith("'"))) {
426
+ return token.slice(1, -1);
427
+ }
428
+ if (token === 'true')
429
+ return true;
430
+ if (token === 'false')
431
+ return false;
432
+ const asNumber = Number(token);
433
+ if (!Number.isNaN(asNumber))
434
+ return asNumber;
435
+ return token;
436
+ }
322
437
  /**
323
438
  * Run all test suites
324
439
  */
@@ -346,18 +461,25 @@ async function runSingleTest(testCase, sdk, options, context) {
346
461
  const { logger } = context;
347
462
  const startTime = Date.now();
348
463
  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
464
+ let bytecode;
465
+ let abi = testCase.inlineAbi;
466
+ if (testCase.inlineBytecode) {
467
+ bytecode = testCase.inlineBytecode;
468
+ }
469
+ else {
470
+ // Load bytecode using centralized manager
471
+ const fileManager = FiveFileManager.getInstance();
472
+ const loadedFile = await fileManager.loadFile(testCase.bytecode, {
473
+ validateFormat: true
474
+ });
475
+ bytecode = loadedFile.bytecode;
476
+ if (!abi) {
477
+ abi = loadedFile.abi;
478
+ }
479
+ }
358
480
  // Parse input parameters if specified
359
- let parameters = [];
360
- if (testCase.input) {
481
+ let parameters = testCase.parameters || [];
482
+ if (parameters.length === 0 && testCase.input) {
361
483
  const inputData = await readFile(testCase.input, 'utf8');
362
484
  try {
363
485
  parameters = JSON.parse(inputData);
@@ -368,12 +490,11 @@ async function runSingleTest(testCase, sdk, options, context) {
368
490
  }
369
491
  }
370
492
  // Execute with timeout using 5IVE SDK
371
- const executionPromise = FiveSDK.executeLocally(bytecode, 0, // Default to first function
372
- parameters, {
493
+ const executionPromise = FiveSDK.executeLocally(bytecode, testCase.functionRef ?? 0, parameters, {
373
494
  debug: options.verbose,
374
495
  trace: options.verbose,
375
496
  computeUnitLimit: options.maxCu,
376
- abi: loadedFile.abi // Pass ABI for function name resolution
497
+ abi // Pass ABI for function name resolution
377
498
  });
378
499
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Test timeout')), options.timeout));
379
500
  const result = await Promise.race([executionPromise, timeoutPromise]);
@@ -528,19 +649,39 @@ async function runOnChainTests(testPath, options, context) {
528
649
  logger.info(`${targetPrefix} Testing on ${config.target}`);
529
650
  logger.info(`Network: ${config.networks[config.target].rpcUrl}`);
530
651
  logger.info(`Keypair: ${config.keypairPath}`);
531
- // Discover .bin test files
652
+ // Discover artifact test files (legacy on-chain mode)
532
653
  const testFiles = await discoverBinFiles(testPath, options);
533
- if (testFiles.length === 0) {
534
- logger.warn('No .bin test files found');
535
- return;
654
+ if (testFiles.length > 0) {
655
+ logger.info(`Found ${testFiles.length} artifact test script(s)`);
536
656
  }
537
- logger.info(`Found ${testFiles.length} test script(s)`);
538
657
  // Setup Solana connection and keypair
539
658
  const connection = new Connection(config.networks[config.target].rpcUrl, 'confirmed');
540
659
  const signerKeypair = await loadKeypair(config.keypairPath);
541
660
  logger.info(`Deployer: ${signerKeypair.publicKey.toString()}`);
542
- // Run batch testing
543
- const results = await runBatchOnChainTests(testFiles, connection, signerKeypair, options, config);
661
+ const maxCostLamports = parseMaxCostLamports(options.maxCostSol);
662
+ if (config.target === 'mainnet') {
663
+ if (!options.allowMainnetTests) {
664
+ throw new Error('mainnet on-chain tests require --allow-mainnet-tests');
665
+ }
666
+ if (maxCostLamports === undefined) {
667
+ throw new Error('mainnet on-chain tests require --max-cost-sol <amount>');
668
+ }
669
+ }
670
+ await ensureOnChainBalance(connection, signerKeypair, config.target, logger);
671
+ const discovered = await TestDiscovery.discoverTests(testPath, { verbose: options.verbose });
672
+ const vTests = discovered.filter((t) => t.type === 'v-source' && t.source);
673
+ let results;
674
+ if (vTests.length > 0) {
675
+ results = await runDiscoveredVOnChainTests(vTests, connection, signerKeypair, options, config, maxCostLamports);
676
+ }
677
+ else {
678
+ // Fall back to artifact-driven on-chain mode
679
+ if (testFiles.length === 0) {
680
+ logger.warn('No on-chain tests discovered (.v test functions or .bin/.five artifacts)');
681
+ return;
682
+ }
683
+ results = await runBatchOnChainTests(testFiles, connection, signerKeypair, options, config, maxCostLamports);
684
+ }
544
685
  // Display comprehensive results
545
686
  displayOnChainTestResults(results, options, logger);
546
687
  // Exit with appropriate code
@@ -557,6 +698,29 @@ async function runOnChainTests(testPath, options, context) {
557
698
  throw error;
558
699
  }
559
700
  }
701
+ function parseMaxCostLamports(raw) {
702
+ if (raw === undefined || raw === null || raw === '')
703
+ return undefined;
704
+ const parsed = Number(raw);
705
+ if (!Number.isFinite(parsed) || parsed <= 0) {
706
+ throw new Error(`Invalid --max-cost-sol value: ${raw}`);
707
+ }
708
+ return Math.floor(parsed * 1_000_000_000);
709
+ }
710
+ async function ensureOnChainBalance(connection, signerKeypair, target, logger) {
711
+ const minLamports = 200_000_000; // 0.2 SOL baseline
712
+ const balance = await connection.getBalance(signerKeypair.publicKey, 'confirmed');
713
+ if (balance >= minLamports) {
714
+ return;
715
+ }
716
+ if (target === 'local' || target === 'localnet') {
717
+ logger.info('Low localnet balance detected; requesting airdrop...');
718
+ const sig = await connection.requestAirdrop(signerKeypair.publicKey, 2_000_000_000);
719
+ await connection.confirmTransaction(sig, 'confirmed');
720
+ return;
721
+ }
722
+ throw new Error(`Insufficient balance for on-chain tests on ${target}. Balance=${(balance / 1e9).toFixed(6)} SOL`);
723
+ }
560
724
  /**
561
725
  * Run tests using modern SDK-based test runner
562
726
  */
@@ -571,7 +735,7 @@ async function runWithSdkRunner(testPath, options, context) {
571
735
  verbose: options.verbose,
572
736
  debug: options.verbose,
573
737
  trace: options.verbose,
574
- pattern: options.filter || '*',
738
+ pattern: options.filter || options.pattern || '*',
575
739
  failFast: false
576
740
  });
577
741
  try {
@@ -703,7 +867,7 @@ async function loadKeypair(keypairPath) {
703
867
  /**
704
868
  * Run batch on-chain tests with deploy → execute → verify pipeline
705
869
  */
706
- async function runBatchOnChainTests(testFiles, connection, signerKeypair, options, config) {
870
+ async function runBatchOnChainTests(testFiles, connection, signerKeypair, options, config, maxCostLamports) {
707
871
  const results = [];
708
872
  const startTime = Date.now();
709
873
  let totalCost = 0;
@@ -763,6 +927,9 @@ async function runBatchOnChainTests(testFiles, connection, signerKeypair, option
763
927
  const testDuration = Date.now() - testStartTime;
764
928
  const testCost = (deployResult.deploymentCost || 0) + (executeResult.cost || 0);
765
929
  totalCost += testCost;
930
+ if (maxCostLamports !== undefined && totalCost > maxCostLamports) {
931
+ throw new Error(`On-chain test cost cap exceeded: ${(totalCost / 1e9).toFixed(6)} SOL > ${(maxCostLamports / 1e9).toFixed(6)} SOL`);
932
+ }
766
933
  const passed = deployResult.success && executeResult.success;
767
934
  if (passed) {
768
935
  spinner.succeed(`[${i + 1}/${testFiles.length}] ${scriptName} OK (${testDuration}ms, ${(testCost / 1e9).toFixed(4)} SOL)`);
@@ -815,6 +982,188 @@ async function runBatchOnChainTests(testFiles, connection, signerKeypair, option
815
982
  results
816
983
  };
817
984
  }
985
+ async function runDiscoveredVOnChainTests(discoveredVTests, connection, signerKeypair, options, config, maxCostLamports) {
986
+ const grouped = new Map();
987
+ for (const test of discoveredVTests) {
988
+ const tests = grouped.get(test.path) || [];
989
+ tests.push(test);
990
+ grouped.set(test.path, tests);
991
+ }
992
+ const results = [];
993
+ let totalCost = 0;
994
+ const start = Date.now();
995
+ for (const [sourceFile, tests] of grouped.entries()) {
996
+ const source = await readFile(sourceFile, 'utf8');
997
+ const compilation = await FiveSDK.compile({ filename: sourceFile, content: source }, { debug: options.verbose, optimize: false });
998
+ if (!compilation.success || !compilation.bytecode) {
999
+ for (const test of tests) {
1000
+ results.push({
1001
+ scriptFile: `${sourceFile}::${test.source.functionName}`,
1002
+ passed: false,
1003
+ totalDuration: 0,
1004
+ totalCost: 0,
1005
+ error: `Compilation failed: ${compilation.errors?.join(', ')}`
1006
+ });
1007
+ }
1008
+ continue;
1009
+ }
1010
+ const fixture = await loadOnChainFixture(sourceFile);
1011
+ const deploy = await FiveSDK.deployToSolana(compilation.bytecode, connection, signerKeypair, {
1012
+ debug: options.verbose || false,
1013
+ network: config.target,
1014
+ computeBudget: 1_000_000,
1015
+ maxRetries: 3
1016
+ });
1017
+ totalCost += deploy.deploymentCost || 0;
1018
+ if (!deploy.success || !deploy.programId) {
1019
+ for (const test of tests) {
1020
+ results.push({
1021
+ scriptFile: `${sourceFile}::${test.source.functionName}`,
1022
+ passed: false,
1023
+ deployResult: {
1024
+ success: false,
1025
+ error: deploy.error,
1026
+ cost: deploy.deploymentCost || 0
1027
+ },
1028
+ totalDuration: 0,
1029
+ totalCost: deploy.deploymentCost || 0,
1030
+ error: `Deployment failed: ${deploy.error || 'unknown error'}`
1031
+ });
1032
+ }
1033
+ continue;
1034
+ }
1035
+ for (const test of tests) {
1036
+ const testStart = Date.now();
1037
+ const fixtureSpec = fixture?.tests?.[test.source.functionName];
1038
+ const expectedSuccess = fixtureSpec?.expected?.success ?? true;
1039
+ const params = fixtureSpec?.parameters ?? test.parameters ?? [];
1040
+ const { accounts, error: fixtureError } = await createPerTestFixtureAccounts(connection, signerKeypair, fixture, fixtureSpec, options.verbose);
1041
+ if (fixtureError) {
1042
+ results.push({
1043
+ scriptFile: `${sourceFile}::${test.source.functionName}`,
1044
+ passed: false,
1045
+ deployResult: {
1046
+ success: true,
1047
+ scriptAccount: deploy.programId,
1048
+ transactionId: deploy.transactionId,
1049
+ cost: deploy.deploymentCost || 0
1050
+ },
1051
+ totalDuration: Date.now() - testStart,
1052
+ totalCost: 0,
1053
+ error: fixtureError
1054
+ });
1055
+ continue;
1056
+ }
1057
+ const execute = await FiveSDK.executeOnSolana(deploy.programId, connection, signerKeypair, test.source.functionName, params, accounts, {
1058
+ debug: options.verbose || false,
1059
+ network: config.target,
1060
+ computeUnitLimit: 1_000_000,
1061
+ maxRetries: 3,
1062
+ abi: compilation.abi
1063
+ });
1064
+ const testCost = execute.cost || 0;
1065
+ totalCost += testCost;
1066
+ if (maxCostLamports !== undefined && totalCost > maxCostLamports) {
1067
+ throw new Error(`On-chain test cost cap exceeded: ${(totalCost / 1e9).toFixed(6)} SOL > ${(maxCostLamports / 1e9).toFixed(6)} SOL`);
1068
+ }
1069
+ const passed = expectedSuccess ? execute.success : !execute.success;
1070
+ const errorContains = fixtureSpec?.expected?.errorContains;
1071
+ const errorMatches = errorContains
1072
+ ? (execute.error || '').includes(errorContains)
1073
+ : true;
1074
+ results.push({
1075
+ scriptFile: `${sourceFile}::${test.source.functionName}`,
1076
+ passed: passed && errorMatches,
1077
+ deployResult: {
1078
+ success: true,
1079
+ scriptAccount: deploy.programId,
1080
+ transactionId: deploy.transactionId,
1081
+ cost: deploy.deploymentCost || 0
1082
+ },
1083
+ executeResult: {
1084
+ success: execute.success || false,
1085
+ transactionId: execute.transactionId,
1086
+ computeUnitsUsed: execute.computeUnitsUsed,
1087
+ cost: execute.cost,
1088
+ result: execute.result,
1089
+ error: execute.error
1090
+ },
1091
+ totalDuration: Date.now() - testStart,
1092
+ totalCost: testCost,
1093
+ error: passed && errorMatches ? undefined : execute.error
1094
+ });
1095
+ }
1096
+ }
1097
+ const passed = results.filter((r) => r.passed).length;
1098
+ const failed = results.length - passed;
1099
+ return {
1100
+ totalScripts: results.length,
1101
+ passed,
1102
+ failed,
1103
+ totalCost,
1104
+ totalDuration: Date.now() - start,
1105
+ results
1106
+ };
1107
+ }
1108
+ async function loadOnChainFixture(sourceFile) {
1109
+ const fixturePath = sourceFile.replace(/\.v$/, '.test.json');
1110
+ try {
1111
+ const content = await readFile(fixturePath, 'utf8');
1112
+ return JSON.parse(content);
1113
+ }
1114
+ catch {
1115
+ return undefined;
1116
+ }
1117
+ }
1118
+ async function createPerTestFixtureAccounts(connection, signerKeypair, fixture, testSpec, verbose) {
1119
+ const required = testSpec?.accounts || [];
1120
+ if (required.length === 0) {
1121
+ return { accounts: [] };
1122
+ }
1123
+ if (!fixture?.accounts) {
1124
+ return { accounts: [], error: 'Fixture accounts are required but no companion .test.json accounts block was found' };
1125
+ }
1126
+ const { PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } = await import('@solana/web3.js');
1127
+ const createdAccounts = [];
1128
+ for (const accountName of required) {
1129
+ const spec = fixture.accounts[accountName];
1130
+ if (!spec) {
1131
+ return { accounts: [], error: `Fixture account '${accountName}' not found in companion fixture file` };
1132
+ }
1133
+ if (spec.is_signer) {
1134
+ return {
1135
+ accounts: [],
1136
+ error: `Fixture account '${accountName}' requests is_signer=true; external signer fixtures are not supported yet`
1137
+ };
1138
+ }
1139
+ const owner = resolveFixtureOwner(spec.owner, signerKeypair.publicKey.toBase58());
1140
+ const dataLen = spec.data_len || 0;
1141
+ const lamports = spec.lamports ?? (await connection.getMinimumBalanceForRentExemption(dataLen));
1142
+ const keypair = Keypair.generate();
1143
+ const tx = new Transaction().add(SystemProgram.createAccount({
1144
+ fromPubkey: signerKeypair.publicKey,
1145
+ newAccountPubkey: keypair.publicKey,
1146
+ lamports,
1147
+ space: dataLen,
1148
+ programId: new PublicKey(owner)
1149
+ }));
1150
+ await sendAndConfirmTransaction(connection, tx, [signerKeypair, keypair], { commitment: 'confirmed' });
1151
+ createdAccounts.push(keypair.publicKey.toBase58());
1152
+ if (verbose) {
1153
+ console.log(`[on-chain fixture] created ${accountName}=${keypair.publicKey.toBase58()}`);
1154
+ }
1155
+ }
1156
+ return { accounts: createdAccounts };
1157
+ }
1158
+ function resolveFixtureOwner(owner, fallback) {
1159
+ if (!owner || owner === 'system') {
1160
+ return '11111111111111111111111111111111';
1161
+ }
1162
+ if (owner === 'payer') {
1163
+ return fallback;
1164
+ }
1165
+ return owner;
1166
+ }
818
1167
  /**
819
1168
  * Display comprehensive on-chain test results
820
1169
  */