@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.
- package/README.md +4 -0
- package/dist/assets/vm/five_vm_wasm_bg.wasm +0 -0
- package/dist/commands/execute.js +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +384 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +400 -51
- package/dist/commands/test.js.map +1 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +3 -0
- package/dist/config/types.js.map +1 -1
- package/dist/wasm/vm.js +1 -1
- package/dist/wasm/vm.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md +11 -0
package/dist/commands/test.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Test command.
|
|
2
2
|
import { readFile, readdir, stat } from 'fs/promises';
|
|
3
|
-
import { join, basename
|
|
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 {
|
|
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
|
|
22
|
-
defaultValue: '
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
256
|
-
if (
|
|
257
|
-
|
|
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:
|
|
267
|
-
|
|
268
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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,
|
|
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
|
|
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
|
|
652
|
+
// Discover artifact test files (legacy on-chain mode)
|
|
532
653
|
const testFiles = await discoverBinFiles(testPath, options);
|
|
533
|
-
if (testFiles.length
|
|
534
|
-
logger.
|
|
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
|
-
|
|
543
|
-
|
|
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
|
*/
|