minovative-mind-cli 2.6.4 → 2.7.0
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 +3 -3
- package/dist/services/orchestration/messageBus.d.ts +1 -0
- package/dist/services/orchestration/messageBus.js +5 -2
- package/dist/services/verificationService.d.ts +42 -0
- package/dist/services/verificationService.js +235 -0
- package/dist/utils/analysisRunner.d.ts +20 -0
- package/dist/utils/analysisRunner.js +117 -0
- package/dist/utils/atomicWrite.js +2 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ and hope for the best.
|
|
|
7
7
|
|
|
8
8
|
This CLI does the opposite — it uses a custom built agentic system called,
|
|
9
9
|
**Precision-Context Verification (PCV)** engine to feed lightweight Flash models
|
|
10
|
-
exactly the right context, execute code, verify
|
|
10
|
+
exactly the right context, execute code, verify compilation, execute Property-Based Testing (PBT) suites and diff-scoped mutation audits, and self-correct (if it even needs to), until
|
|
11
11
|
the build/performance metrics are green.
|
|
12
12
|
|
|
13
13
|
> The result: **Genuine Pro reasoning accuracy at Flash-level speed and efficiency.**
|
|
@@ -142,7 +142,7 @@ By prefixing file paths with `@alias/` (e.g., `@backend/src/api.ts` and `@fronte
|
|
|
142
142
|
|
|
143
143
|
Minovative Mind CLI fundamentally supports **ALL programming languages** for chat, code generation, planning, and execution, as it relies on Gemini's vast training data.
|
|
144
144
|
|
|
145
|
-
However, the PCV engine features deep, context-aware analysis across **12 major programming language families**. Our core advanced engines—**Smart Dependency Tracing**, **Performance Auditing** (across 9 language families), and **Ephemeral Analysis Scripts**—provide tailored support depending on the language's syntax and runtime model:
|
|
145
|
+
However, the PCV engine features deep, context-aware analysis across **12 major programming language families**. Our core advanced engines—**Smart Dependency Tracing**, **Property-Based & Mutation Verification**, **Performance Auditing** (across 9 language families), and **Ephemeral Analysis Scripts**—provide tailored support depending on the language's syntax and runtime model:
|
|
146
146
|
|
|
147
147
|
| Language Family | Supported Extensions | Dependency Tracing | Performance Auditing | Ephemeral Analysis |
|
|
148
148
|
| :--------------------------- | :------------------------------------------- | :----------------: | :------------------: | :----------------: |
|
|
@@ -165,7 +165,7 @@ _⚠️\*Support is limited or requires custom local system tooling/environment
|
|
|
165
165
|
|
|
166
166
|
1. **Dependency Tracing:** Fully supported across language families. It uses lightning-fast static regex pattern matching to resolve local import structures and determine the "blast radius" of code changes without requiring local compilations.
|
|
167
167
|
2. **Performance Auditing:** Executed natively across 9 major language families (JavaScript/TypeScript, Python, Go, Rust, PHP, C#, Java, C/C++, and Ruby). It uses zero-dependency, ultra-fast (<50ms) language-aware regex heuristics with comment/string stripping and line-preserving offset tracking to detect severe runtime anti-patterns (such as unbounded loops, synchronous I/O, chained array allocations, unnecessary allocations, and unclosed resources).
|
|
168
|
-
3. **Ephemeral Analysis
|
|
168
|
+
3. **Ephemeral Analysis & Property-Based Probing:** Runs temporary files in sandbox directories to parse and extract local symbols, execute ephemeral Property-Based Testing (PBT) probes to discover and shrink counterexamples, perform infrastructure probing (detecting available runtimes, checking ports, identifying project type), post-change validation (importing modified modules and asserting expected behavior), and lightweight ML-powered analysis (e.g., TF-IDF file ranking, anomaly detection, Big-O estimation). Defaults to Node.js as a safe baseline, but natively leverages host-installed runtimes (Python, Rust, Go compilers) and standard libraries whenever available in the workspace.
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
|
@@ -35,6 +35,7 @@ export class MessageBus {
|
|
|
35
35
|
signals = [];
|
|
36
36
|
activityCursors = new Map();
|
|
37
37
|
signalCursors = new Map();
|
|
38
|
+
persistQueue = Promise.resolve();
|
|
38
39
|
persistPath;
|
|
39
40
|
workspaceRoot;
|
|
40
41
|
/** Maximum semantic signals any single agent can post */
|
|
@@ -172,8 +173,10 @@ export class MessageBus {
|
|
|
172
173
|
activityCursors: Object.fromEntries(this.activityCursors),
|
|
173
174
|
signalCursors: Object.fromEntries(this.signalCursors),
|
|
174
175
|
};
|
|
175
|
-
// Fire-and-forget — don't block the calling agent's tool loop
|
|
176
|
-
|
|
176
|
+
// Fire-and-forget — don't block the calling agent's tool loop, but serialize writes sequentially
|
|
177
|
+
this.persistQueue = this.persistQueue
|
|
178
|
+
.then(() => atomicWriteFile(this.persistPath, JSON.stringify(snapshot)))
|
|
179
|
+
.catch((err) => {
|
|
177
180
|
debugLog(`MessageBus: Failed to persist to disk: ${err}`);
|
|
178
181
|
});
|
|
179
182
|
}
|
|
@@ -5,11 +5,53 @@ export interface VerificationResult {
|
|
|
5
5
|
errors: string[];
|
|
6
6
|
aborted?: boolean;
|
|
7
7
|
}
|
|
8
|
+
export interface Mutant {
|
|
9
|
+
id: string;
|
|
10
|
+
filePath: string;
|
|
11
|
+
line: number;
|
|
12
|
+
originalCode: string;
|
|
13
|
+
mutatedCode: string;
|
|
14
|
+
operator: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MutationTestResult {
|
|
17
|
+
totalMutants: number;
|
|
18
|
+
killedMutants: number;
|
|
19
|
+
survivedMutants: number;
|
|
20
|
+
mutationScore: number;
|
|
21
|
+
mutants: Array<Mutant & {
|
|
22
|
+
killed: boolean;
|
|
23
|
+
output?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
8
26
|
export declare function detectVerificationCommand(workspaceRoot: string): Promise<string | null>;
|
|
9
27
|
export declare function runVerification(workspaceRoot: string, abortSignal?: AbortSignal): Promise<VerificationResult | null>;
|
|
10
28
|
export declare function formatVerificationForModel(result: VerificationResult): string;
|
|
11
29
|
export interface FullVerificationResult {
|
|
12
30
|
errors: string | null;
|
|
13
31
|
warnings: string | null;
|
|
32
|
+
mutationResult?: MutationTestResult | null;
|
|
14
33
|
}
|
|
15
34
|
export declare function verifyChangedFiles(workspaceRoot: string, filePaths: string[], abortSignal?: AbortSignal): Promise<FullVerificationResult | '[Verification Aborted]'>;
|
|
35
|
+
/**
|
|
36
|
+
* Generates mutants scoped specifically to modified lines of a source file.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateDiffScopedMutants(filePath: string, content: string, modifiedLines?: number[]): Mutant[];
|
|
39
|
+
/**
|
|
40
|
+
* Runs diff-scoped mutation testing on modified files within the workspace.
|
|
41
|
+
* Mutates target lines in-memory/temp copy, executes workspace tests, and ensures mutants are killed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runDiffScopedMutationTesting(workspaceRoot: string, filePaths: string[], modifiedLineMap?: Record<string, number[]>, abortSignal?: AbortSignal, options?: {
|
|
44
|
+
maxMutantsPerFile?: number;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}): Promise<MutationTestResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Detects whether property-based testing framework configuration or dependencies exist in workspace.
|
|
49
|
+
*/
|
|
50
|
+
export declare function detectPropertyBasedTests(workspaceRoot: string): Promise<{
|
|
51
|
+
hasPbt: boolean;
|
|
52
|
+
framework?: string;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Executes workspace property-based tests if detected.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runPropertyBasedVerification(workspaceRoot: string, abortSignal?: AbortSignal): Promise<VerificationResult | null>;
|
|
@@ -299,3 +299,238 @@ export async function verifyChangedFiles(workspaceRoot, filePaths, abortSignal)
|
|
|
299
299
|
warnings: terminalWarnings || null,
|
|
300
300
|
};
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Common mutation operators for AST/line-level diff mutation testing.
|
|
304
|
+
*/
|
|
305
|
+
const MUTATION_OPERATORS = [
|
|
306
|
+
{
|
|
307
|
+
name: 'EqualityReplacement',
|
|
308
|
+
pattern: /(===|!==|==|!=)/g,
|
|
309
|
+
replace: (m) => {
|
|
310
|
+
if (m === '===')
|
|
311
|
+
return '!==';
|
|
312
|
+
if (m === '!==')
|
|
313
|
+
return '===';
|
|
314
|
+
if (m === '==')
|
|
315
|
+
return '!=';
|
|
316
|
+
if (m === '!=')
|
|
317
|
+
return '==';
|
|
318
|
+
return m;
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'RelationalReplacement',
|
|
323
|
+
pattern: /(>=|<=|>|<)/g,
|
|
324
|
+
replace: (m) => {
|
|
325
|
+
if (m === '>=')
|
|
326
|
+
return '<';
|
|
327
|
+
if (m === '<=')
|
|
328
|
+
return '>';
|
|
329
|
+
if (m === '>')
|
|
330
|
+
return '<=';
|
|
331
|
+
if (m === '<')
|
|
332
|
+
return '>=';
|
|
333
|
+
return m;
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'ArithmeticReplacement',
|
|
338
|
+
pattern: /(\+|\-|\*|\/)/g,
|
|
339
|
+
replace: (m) => {
|
|
340
|
+
if (m === '+')
|
|
341
|
+
return '-';
|
|
342
|
+
if (m === '-')
|
|
343
|
+
return '+';
|
|
344
|
+
if (m === '*')
|
|
345
|
+
return '/';
|
|
346
|
+
if (m === '/')
|
|
347
|
+
return '*';
|
|
348
|
+
return m;
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'LogicalReplacement',
|
|
353
|
+
pattern: /(\&\&|\|\|)/g,
|
|
354
|
+
replace: (m) => (m === '&&' ? '||' : '&&'),
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'BooleanLiteralInversion',
|
|
358
|
+
pattern: /\b(true|false)\b/g,
|
|
359
|
+
replace: (m) => (m === 'true' ? 'false' : 'true'),
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
/**
|
|
363
|
+
* Generates mutants scoped specifically to modified lines of a source file.
|
|
364
|
+
*/
|
|
365
|
+
export function generateDiffScopedMutants(filePath, content, modifiedLines) {
|
|
366
|
+
const mutants = [];
|
|
367
|
+
const lines = content.split('\n');
|
|
368
|
+
const lineNumbers = modifiedLines && modifiedLines.length > 0
|
|
369
|
+
? modifiedLines
|
|
370
|
+
: lines.map((_, i) => i + 1);
|
|
371
|
+
let idCounter = 1;
|
|
372
|
+
for (const lineNum of lineNumbers) {
|
|
373
|
+
if (lineNum < 1 || lineNum > lines.length)
|
|
374
|
+
continue;
|
|
375
|
+
const origLine = lines[lineNum - 1];
|
|
376
|
+
// Skip empty lines, comments, or imports/exports
|
|
377
|
+
const trimmed = origLine.trim();
|
|
378
|
+
if (!trimmed ||
|
|
379
|
+
trimmed.startsWith('//') ||
|
|
380
|
+
trimmed.startsWith('/*') ||
|
|
381
|
+
trimmed.startsWith('*') ||
|
|
382
|
+
trimmed.startsWith('import ') ||
|
|
383
|
+
trimmed.startsWith('export ') ||
|
|
384
|
+
trimmed.startsWith('#')) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
for (const op of MUTATION_OPERATORS) {
|
|
388
|
+
op.pattern.lastIndex = 0;
|
|
389
|
+
let match;
|
|
390
|
+
while ((match = op.pattern.exec(origLine)) !== null) {
|
|
391
|
+
const start = match.index;
|
|
392
|
+
const matchedText = match[0];
|
|
393
|
+
const replacement = op.replace(matchedText);
|
|
394
|
+
if (replacement === matchedText)
|
|
395
|
+
continue;
|
|
396
|
+
const mutatedLine = origLine.substring(0, start) + replacement + origLine.substring(start + matchedText.length);
|
|
397
|
+
mutants.push({
|
|
398
|
+
id: `mutant-${idCounter++}`,
|
|
399
|
+
filePath,
|
|
400
|
+
line: lineNum,
|
|
401
|
+
originalCode: origLine,
|
|
402
|
+
mutatedCode: mutatedLine,
|
|
403
|
+
operator: op.name,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return mutants;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Runs diff-scoped mutation testing on modified files within the workspace.
|
|
412
|
+
* Mutates target lines in-memory/temp copy, executes workspace tests, and ensures mutants are killed.
|
|
413
|
+
*/
|
|
414
|
+
export async function runDiffScopedMutationTesting(workspaceRoot, filePaths, modifiedLineMap, abortSignal, options) {
|
|
415
|
+
const maxPerFile = options?.maxMutantsPerFile ?? 10;
|
|
416
|
+
const allMutants = [];
|
|
417
|
+
let totalKilled = 0;
|
|
418
|
+
for (const relPath of filePaths) {
|
|
419
|
+
if (abortSignal?.aborted)
|
|
420
|
+
break;
|
|
421
|
+
const fullPath = path.resolve(workspaceRoot, relPath);
|
|
422
|
+
let originalContent;
|
|
423
|
+
try {
|
|
424
|
+
originalContent = await fs.readFile(fullPath, 'utf-8');
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const modifiedLines = modifiedLineMap ? modifiedLineMap[relPath] : undefined;
|
|
430
|
+
const mutants = generateDiffScopedMutants(relPath, originalContent, modifiedLines).slice(0, maxPerFile);
|
|
431
|
+
const lines = originalContent.split('\n');
|
|
432
|
+
for (const mutant of mutants) {
|
|
433
|
+
if (abortSignal?.aborted)
|
|
434
|
+
break;
|
|
435
|
+
// Create mutated file content
|
|
436
|
+
const mutatedLines = [...lines];
|
|
437
|
+
mutatedLines[mutant.line - 1] = mutant.mutatedCode;
|
|
438
|
+
const mutatedContent = mutatedLines.join('\n');
|
|
439
|
+
let killed = false;
|
|
440
|
+
let output = '';
|
|
441
|
+
try {
|
|
442
|
+
// Safely write mutated content with try...finally cleanup
|
|
443
|
+
await fs.writeFile(fullPath, mutatedContent, 'utf-8');
|
|
444
|
+
// Execute verification/test command
|
|
445
|
+
const testResult = await runVerification(workspaceRoot, abortSignal);
|
|
446
|
+
if (testResult && !testResult.success) {
|
|
447
|
+
killed = true;
|
|
448
|
+
output = testResult.errors.join('\n').substring(0, 500);
|
|
449
|
+
}
|
|
450
|
+
else if (!testResult) {
|
|
451
|
+
// No test runner detected; default to non-kill unless error occurs
|
|
452
|
+
killed = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
killed = true;
|
|
457
|
+
output = err.message || String(err);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
// Restore original file unconditionally
|
|
461
|
+
try {
|
|
462
|
+
await fs.writeFile(fullPath, originalContent, 'utf-8');
|
|
463
|
+
}
|
|
464
|
+
catch (restoreErr) {
|
|
465
|
+
debugLog(`Failed to restore ${fullPath} after mutation testing: ${restoreErr}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (killed)
|
|
469
|
+
totalKilled++;
|
|
470
|
+
allMutants.push({ ...mutant, killed, output });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const total = allMutants.length;
|
|
474
|
+
const score = total > 0 ? Math.round((totalKilled / total) * 100) : 100;
|
|
475
|
+
return {
|
|
476
|
+
totalMutants: total,
|
|
477
|
+
killedMutants: totalKilled,
|
|
478
|
+
survivedMutants: total - totalKilled,
|
|
479
|
+
mutationScore: score,
|
|
480
|
+
mutants: allMutants,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Detects whether property-based testing framework configuration or dependencies exist in workspace.
|
|
485
|
+
*/
|
|
486
|
+
export async function detectPropertyBasedTests(workspaceRoot) {
|
|
487
|
+
// Check package.json for fast-check or jsverify
|
|
488
|
+
try {
|
|
489
|
+
const pkgStr = await fs.readFile(path.join(workspaceRoot, 'package.json'), 'utf-8');
|
|
490
|
+
if (pkgStr.includes('fast-check'))
|
|
491
|
+
return { hasPbt: true, framework: 'fast-check' };
|
|
492
|
+
if (pkgStr.includes('jsverify'))
|
|
493
|
+
return { hasPbt: true, framework: 'jsverify' };
|
|
494
|
+
}
|
|
495
|
+
catch { }
|
|
496
|
+
// Check Python environment for hypothesis
|
|
497
|
+
try {
|
|
498
|
+
const reqStr = await fs.readFile(path.join(workspaceRoot, 'requirements.txt'), 'utf-8');
|
|
499
|
+
if (reqStr.includes('hypothesis'))
|
|
500
|
+
return { hasPbt: true, framework: 'hypothesis' };
|
|
501
|
+
}
|
|
502
|
+
catch { }
|
|
503
|
+
try {
|
|
504
|
+
const pyprojStr = await fs.readFile(path.join(workspaceRoot, 'pyproject.toml'), 'utf-8');
|
|
505
|
+
if (pyprojStr.includes('hypothesis'))
|
|
506
|
+
return { hasPbt: true, framework: 'hypothesis' };
|
|
507
|
+
}
|
|
508
|
+
catch { }
|
|
509
|
+
// Check Cargo.toml for proptest / quickcheck
|
|
510
|
+
try {
|
|
511
|
+
const cargoStr = await fs.readFile(path.join(workspaceRoot, 'Cargo.toml'), 'utf-8');
|
|
512
|
+
if (cargoStr.includes('proptest'))
|
|
513
|
+
return { hasPbt: true, framework: 'proptest' };
|
|
514
|
+
if (cargoStr.includes('quickcheck'))
|
|
515
|
+
return { hasPbt: true, framework: 'quickcheck' };
|
|
516
|
+
}
|
|
517
|
+
catch { }
|
|
518
|
+
// Check go.mod for rapid
|
|
519
|
+
try {
|
|
520
|
+
const goModStr = await fs.readFile(path.join(workspaceRoot, 'go.mod'), 'utf-8');
|
|
521
|
+
if (goModStr.includes('pgregory.net/rapid'))
|
|
522
|
+
return { hasPbt: true, framework: 'rapid' };
|
|
523
|
+
}
|
|
524
|
+
catch { }
|
|
525
|
+
return { hasPbt: false };
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Executes workspace property-based tests if detected.
|
|
529
|
+
*/
|
|
530
|
+
export async function runPropertyBasedVerification(workspaceRoot, abortSignal) {
|
|
531
|
+
const pbtInfo = await detectPropertyBasedTests(workspaceRoot);
|
|
532
|
+
if (!pbtInfo.hasPbt)
|
|
533
|
+
return null;
|
|
534
|
+
debugLog(`Detected property-based testing framework: ${pbtInfo.framework}`);
|
|
535
|
+
return runVerification(workspaceRoot, abortSignal);
|
|
536
|
+
}
|
|
@@ -11,6 +11,18 @@ export interface EphemeralScriptOptions {
|
|
|
11
11
|
/** AbortSignal to cancel execution. */
|
|
12
12
|
abortSignal?: AbortSignal;
|
|
13
13
|
}
|
|
14
|
+
export interface PropertyTestConfig {
|
|
15
|
+
numRuns?: number;
|
|
16
|
+
seed?: number;
|
|
17
|
+
shrinkTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PropertyTestResult extends EphemeralScriptResult {
|
|
20
|
+
passed: boolean;
|
|
21
|
+
counterexample?: string;
|
|
22
|
+
shrunkInput?: string;
|
|
23
|
+
seed?: number;
|
|
24
|
+
numRunsCompleted?: number;
|
|
25
|
+
}
|
|
14
26
|
/**
|
|
15
27
|
* Normalizes user/AI provided language string to a standard runtime identifier.
|
|
16
28
|
*/
|
|
@@ -50,3 +62,11 @@ export declare function runEphemeralScript(workspaceRoot: string, language: stri
|
|
|
50
62
|
* @param options - Execution options (timeoutMs, maxOutputChars, abortSignal).
|
|
51
63
|
*/
|
|
52
64
|
export declare function runDebugScript(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, options?: EphemeralScriptOptions): Promise<EphemeralScriptResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Generates an ephemeral Property-Based Testing script template tailored to the target language and properties.
|
|
67
|
+
*/
|
|
68
|
+
export declare function generatePBTScriptTemplate(language: string, targetFunctionOrFeature: string, properties: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
|
|
71
|
+
*/
|
|
72
|
+
export declare function runPropertyBasedTest(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, config?: PropertyTestConfig & EphemeralScriptOptions): Promise<PropertyTestResult>;
|
|
@@ -207,3 +207,120 @@ export async function runDebugScript(workspaceRoot, code, language = 'node', opt
|
|
|
207
207
|
}
|
|
208
208
|
return runEphemeralScript(workspaceRoot, lang, code, opts);
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Generates an ephemeral Property-Based Testing script template tailored to the target language and properties.
|
|
212
|
+
*/
|
|
213
|
+
export function generatePBTScriptTemplate(language, targetFunctionOrFeature, properties) {
|
|
214
|
+
const normLang = normalizeLanguage(language);
|
|
215
|
+
if (normLang === 'python') {
|
|
216
|
+
return `import sys, random
|
|
217
|
+
|
|
218
|
+
# Target: ${targetFunctionOrFeature}
|
|
219
|
+
# Properties to verify:
|
|
220
|
+
${properties.map((p) => `# - ${p}`).join('\n')}
|
|
221
|
+
|
|
222
|
+
def property_test():
|
|
223
|
+
runs = 100
|
|
224
|
+
for i in range(runs):
|
|
225
|
+
# Generate random inputs
|
|
226
|
+
val_int = random.randint(-10000, 10000)
|
|
227
|
+
val_str = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=random.randint(0, 50)))
|
|
228
|
+
|
|
229
|
+
# Test property invariants
|
|
230
|
+
try:
|
|
231
|
+
# Assertion placeholder
|
|
232
|
+
assert isinstance(val_int, int)
|
|
233
|
+
except AssertionError as e:
|
|
234
|
+
print(f"[PBT FAILED] Seed/Iteration: {i}")
|
|
235
|
+
print(f"Counterexample: val_int={val_int}, val_str={val_str!r}")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
|
|
238
|
+
print(f"[PBT PASSED] Completed {runs} runs successfully.")
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
property_test()
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
// Default Node / TypeScript / JS template
|
|
245
|
+
return `// Target: ${targetFunctionOrFeature}
|
|
246
|
+
// Properties to verify:
|
|
247
|
+
${properties.map((p) => `// - ${p}`).join('\n')}
|
|
248
|
+
|
|
249
|
+
async function runPBT() {
|
|
250
|
+
const numRuns = 100;
|
|
251
|
+
let passed = 0;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < numRuns; i++) {
|
|
254
|
+
// Generate pseudo-random inputs
|
|
255
|
+
const randInt = Math.floor(Math.random() * 20000) - 10000;
|
|
256
|
+
const randStr = Math.random().toString(36).substring(2);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// Test property invariant
|
|
260
|
+
if (typeof randInt !== 'number' || isNaN(randInt)) {
|
|
261
|
+
throw new Error('Type invariant failed');
|
|
262
|
+
}
|
|
263
|
+
passed++;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.log(\`[PBT FAILED] Iteration: \${i}\`);
|
|
266
|
+
console.log(\`Counterexample: randInt=\${randInt}, randStr="\${randStr}"\`);
|
|
267
|
+
console.log(\`Error: \${err.message}\`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(\`[PBT PASSED] Completed \${passed} runs successfully.\`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
runPBT();
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
|
|
280
|
+
*/
|
|
281
|
+
export async function runPropertyBasedTest(workspaceRoot, code, language = 'node', config) {
|
|
282
|
+
let lang = 'node';
|
|
283
|
+
let opts;
|
|
284
|
+
if (typeof language === 'string') {
|
|
285
|
+
lang = language;
|
|
286
|
+
opts = config;
|
|
287
|
+
}
|
|
288
|
+
else if (typeof language === 'object' && language !== null) {
|
|
289
|
+
opts = language;
|
|
290
|
+
}
|
|
291
|
+
const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
|
|
292
|
+
const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
|
|
293
|
+
const isPassed = combinedOutput.includes('[PBT PASSED]') || (scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
|
|
294
|
+
let counterexample;
|
|
295
|
+
let shrunkInput;
|
|
296
|
+
let seed;
|
|
297
|
+
let numRunsCompleted;
|
|
298
|
+
// Extract counterexample
|
|
299
|
+
const counterMatch = combinedOutput.match(/Counterexample:\s*([^\n]+)/i);
|
|
300
|
+
if (counterMatch) {
|
|
301
|
+
counterexample = counterMatch[1].trim();
|
|
302
|
+
}
|
|
303
|
+
// Extract shrunk input
|
|
304
|
+
const shrinkMatch = combinedOutput.match(/(?:Shrunk Input|Shrunk|Minimal):\s*([^\n]+)/i);
|
|
305
|
+
if (shrinkMatch) {
|
|
306
|
+
shrunkInput = shrinkMatch[1].trim();
|
|
307
|
+
}
|
|
308
|
+
// Extract seed
|
|
309
|
+
const seedMatch = combinedOutput.match(/Seed(?:|\/Iteration):\s*(\d+)/i);
|
|
310
|
+
if (seedMatch) {
|
|
311
|
+
seed = parseInt(seedMatch[1], 10);
|
|
312
|
+
}
|
|
313
|
+
// Extract completed runs
|
|
314
|
+
const runsMatch = combinedOutput.match(/Completed\s+(\d+)\s+runs/i);
|
|
315
|
+
if (runsMatch) {
|
|
316
|
+
numRunsCompleted = parseInt(runsMatch[1], 10);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
...scriptResult,
|
|
320
|
+
passed: isPassed,
|
|
321
|
+
counterexample,
|
|
322
|
+
shrunkInput,
|
|
323
|
+
seed,
|
|
324
|
+
numRunsCompleted,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -7,7 +7,8 @@ import path from 'node:path';
|
|
|
7
7
|
*/
|
|
8
8
|
export async function atomicWriteFile(targetPath, data, encoding = 'utf-8') {
|
|
9
9
|
const dir = path.dirname(targetPath);
|
|
10
|
-
const
|
|
10
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
11
|
+
const tempPath = path.join(dir, `.${path.basename(targetPath)}.${Date.now()}.${randomSuffix}.tmp`);
|
|
11
12
|
try {
|
|
12
13
|
// Ensure the directory exists
|
|
13
14
|
await fs.mkdir(dir, { recursive: true });
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED