diff --git a/.env.example b/.env.example index 8839be6..f253997 100644 --- a/.env.example +++ b/.env.example @@ -22,3 +22,9 @@ VECTOR_DB_ENABLED=false # VECTOR_DB_TYPE=pinecone|weaviate|chroma # VECTOR_DB_API_KEY=your-api-key # VECTOR_DB_ENDPOINT=https://your-instance.vectordb.com + +# Sandbox Configuration (Optional) +# Set SANDBOX_ENABLED=true to enable sandbox features for diagnostics and simulations +SANDBOX_ENABLED=false +# SANDBOX_DEFAULT_TIMEOUT=30000 # Timeout in milliseconds (default: 30000) +# SANDBOX_LOG_LEVEL=info # Log level for sandbox execution (debug|info|warn|error) diff --git a/README.md b/README.md index 2139c92..d0af706 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This project includes comprehensive quality and hardening features: - **Config Validation**: Type-safe configuration with Zod validation - **Telemetry**: Optional OpenTelemetry integration (no vendor lock-in) +- **Sandboxes**: Isolated execution environments for diagnostics and simulations - **Security Scanning**: Trivy vulnerability scanning and SBOM generation - **Pre-commit Hooks**: Automatic linting and formatting - **Dependency Management**: Renovate for automated updates @@ -59,6 +60,7 @@ The project uses Zod for runtime configuration validation. Configuration is load - `TELEMETRY_*`: Optional telemetry settings - `AI_*`: Optional AI/ML settings - `VECTOR_DB_*`: Optional vector database settings +- `SANDBOX_*`: Optional sandbox settings **Example:** @@ -120,6 +122,142 @@ const span = tracer.startSpan('my-operation'); span.end(); ``` +## Sandboxes + +### Overview + +Sandboxes provide isolated execution environments for running diagnostics and simulations. This feature is useful for transparency testing, scenario validation, and safe execution of potentially long-running or resource-intensive operations. + +**Enable Sandboxes:** + +```bash +# Set in .env file +SANDBOX_ENABLED=true +SANDBOX_DEFAULT_TIMEOUT=30000 # Optional - timeout in milliseconds +SANDBOX_LOG_LEVEL=info # Optional - debug|info|warn|error +``` + +### Features + +- **Isolated Execution**: Run code in isolated sandbox environments +- **Timeout Control**: Automatic timeout handling for long-running operations +- **Error Handling**: Comprehensive error capture and reporting +- **Logging**: Built-in logging with configurable levels +- **Diagnostics**: Run transparency validation tests +- **Simulations**: Execute scenario-based testing + +### Basic Usage + +```typescript +import { runInSandbox } from './sandbox'; + +// Simple sandbox execution +const result = await runInSandbox('my-sandbox', async () => { + // Your code here + return { status: 'success' }; +}); + +if (result.success) { + console.log('Result:', result.data); + console.log('Duration:', result.duration, 'ms'); +} else { + console.error('Error:', result.error); +} +``` + +### Diagnostic Tests + +Run diagnostic tests for transparency validation: + +```typescript +import { runDiagnostics } from './sandbox/diagnostics'; + +const results = await runDiagnostics([ + { + name: 'config-validation', + description: 'Validate configuration', + run: async () => { + // Your validation logic + return true; // pass or false to fail + }, + }, + { + name: 'connectivity-check', + description: 'Check connectivity', + run: async () => { + // Check external services + return true; + }, + }, +]); + +// Results contain: name, status, message, duration, metadata +results.forEach((r) => { + console.log(`${r.name}: ${r.status} (${r.duration}ms)`); +}); +``` + +### Simulations + +Run scenario-based simulations: + +```typescript +import { runSimulations } from './sandbox/simulation'; + +const results = await runSimulations([ + { + name: 'high-load-scenario', + description: 'Test under high load', + input: { events: 1000 }, + run: async (input) => { + // Process events + return { processed: input.events }; + }, + validate: (output) => { + // Optional validation + return output.processed === 1000; + }, + }, +]); + +// Results contain: scenarioName, success, output, duration, validated +results.forEach((r) => { + console.log(`${r.scenarioName}: ${r.success ? 'PASS' : 'FAIL'}`); +}); +``` + +### Advanced Usage + +For more control, use the Sandbox class directly: + +```typescript +import { Sandbox } from './sandbox'; + +const sandbox = new Sandbox({ + name: 'custom-sandbox', + timeout: 60000, // 60 seconds + logLevel: 'debug', + isolated: true, +}); + +const result = await sandbox.execute(async () => { + // Your code here + return { result: 'data' }; +}); + +// Access sandbox logs +const logs = sandbox.getLogs(); +``` + +### Examples + +See `src/sandbox/examples.ts` for complete working examples including: + +- Basic sandbox execution +- Transparency diagnostics +- Load testing simulations +- Error handling scenarios + ## Development ### Building @@ -256,7 +394,15 @@ Check Issues tab for the Renovate Dependency Dashboard │ │ └── validator.ts # Smoke test script │ ├── telemetry/ # OpenTelemetry integration │ │ └── index.ts # Tracer and logger setup +│ ├── sandbox/ # Sandbox execution environments +│ │ ├── index.ts # Core sandbox functionality +│ │ ├── diagnostics.ts # Diagnostic test runner +│ │ ├── simulation.ts # Simulation scenario runner +│ │ └── examples.ts # Usage examples │ └── index.ts # Main entry point +├── test/ # Test files +│ ├── basic.test.js # Basic tests +│ └── sandbox.test.js # Sandbox tests ├── .github/ │ └── workflows/ # GitHub Actions workflows ├── .husky/ # Git hooks diff --git a/src/config/index.ts b/src/config/index.ts index 61c983c..6500494 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -38,6 +38,15 @@ export const ConfigSchema = z.object({ endpoint: z.string().url().optional(), }) .optional(), + + // Sandbox settings (optional) + sandbox: z + .object({ + enabled: z.coerce.boolean().default(false), + defaultTimeout: z.coerce.number().int().positive().default(30000), + logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), + }) + .optional(), }); export type Config = z.infer; @@ -70,6 +79,11 @@ export function loadConfig(): Config { apiKey: process.env.VECTOR_DB_API_KEY, endpoint: process.env.VECTOR_DB_ENDPOINT, }, + sandbox: { + enabled: process.env.SANDBOX_ENABLED, + defaultTimeout: process.env.SANDBOX_DEFAULT_TIMEOUT, + logLevel: process.env.SANDBOX_LOG_LEVEL, + }, }; // Parse and validate configuration diff --git a/src/sandbox/diagnostics.ts b/src/sandbox/diagnostics.ts new file mode 100644 index 0000000..f10b8ef --- /dev/null +++ b/src/sandbox/diagnostics.ts @@ -0,0 +1,146 @@ +/** + * Diagnostic runner for transparency testing + */ + +import { Sandbox, SandboxResult } from './index'; +import { createLogger } from '../telemetry'; + +const logger = createLogger('diagnostics'); + +/** + * Diagnostic test result + */ +export interface DiagnosticResult { + name: string; + status: 'pass' | 'fail' | 'skip'; + message: string; + duration: number; + metadata?: Record; +} + +/** + * Diagnostic test definition + */ +export interface DiagnosticTest { + name: string; + description: string; + run: () => Promise | boolean; + skip?: boolean; +} + +/** + * Diagnostic runner for executing transparency tests + */ +export class DiagnosticRunner { + private tests: DiagnosticTest[] = []; + private sandbox: Sandbox; + + constructor(name: string = 'diagnostic-runner') { + this.sandbox = new Sandbox({ + name, + timeout: 60000, // 60s for diagnostics + logLevel: 'info', + }); + } + + /** + * Register a diagnostic test + */ + registerTest(test: DiagnosticTest): void { + this.tests.push(test); + logger.debug(`Registered diagnostic test: ${test.name}`); + } + + /** + * Run all registered diagnostic tests + */ + async runAll(): Promise { + logger.info(`Running ${this.tests.length} diagnostic tests`); + const results: DiagnosticResult[] = []; + + for (const test of this.tests) { + if (test.skip) { + results.push({ + name: test.name, + status: 'skip', + message: 'Test skipped', + duration: 0, + }); + continue; + } + + const result = await this.runTest(test); + results.push(result); + } + + const passed = results.filter((r) => r.status === 'pass').length; + const failed = results.filter((r) => r.status === 'fail').length; + const skipped = results.filter((r) => r.status === 'skip').length; + + logger.info('Diagnostic tests completed', { + total: results.length, + passed, + failed, + skipped, + }); + + return results; + } + + /** + * Run a single diagnostic test + */ + private async runTest(test: DiagnosticTest): Promise { + const startTime = Date.now(); + + try { + const result: SandboxResult = await this.sandbox.execute(async () => { + return await test.run(); + }); + + const duration = Date.now() - startTime; + + if (!result.success) { + return { + name: test.name, + status: 'fail', + message: result.error?.message || 'Test execution failed', + duration, + metadata: { error: result.error }, + }; + } + + return { + name: test.name, + status: result.data === true ? 'pass' : 'fail', + message: result.data === true ? 'Test passed' : 'Test failed', + duration, + }; + } catch (error) { + const duration = Date.now() - startTime; + return { + name: test.name, + status: 'fail', + message: error instanceof Error ? error.message : 'Unknown error', + duration, + metadata: { error }, + }; + } + } + + /** + * Get number of registered tests + */ + getTestCount(): number { + return this.tests.length; + } +} + +/** + * Helper function to create and run diagnostics + */ +export async function runDiagnostics(tests: DiagnosticTest[]): Promise { + const runner = new DiagnosticRunner(); + tests.forEach((test) => runner.registerTest(test)); + return runner.runAll(); +} diff --git a/src/sandbox/examples.ts b/src/sandbox/examples.ts new file mode 100644 index 0000000..114d8ff --- /dev/null +++ b/src/sandbox/examples.ts @@ -0,0 +1,171 @@ +/** + * Example usage of sandbox functionality + * This file demonstrates how to use sandboxes for diagnostics and simulations + */ + +import { runInSandbox } from './index'; +import { runDiagnostics, DiagnosticTest } from './diagnostics'; +import { runSimulations, SimulationScenario } from './simulation'; +import { createLogger } from '../telemetry'; + +const logger = createLogger('sandbox-examples'); + +/** + * Example 1: Basic sandbox usage + */ +export async function exampleBasicSandbox() { + logger.info('Running basic sandbox example...'); + + const result = await runInSandbox('basic-example', async () => { + // Simulated transparency check + const transparencyScore = Math.random(); + return { + transparent: transparencyScore > 0.5, + score: transparencyScore, + }; + }); + + if (result.success) { + logger.info('Transparency check completed', result.data); + } else { + logger.error('Transparency check failed', result.error); + } + + return result; +} + +/** + * Example 2: Diagnostic tests for transparency validation + */ +export async function exampleTransparencyDiagnostics() { + logger.info('Running transparency diagnostics...'); + + const diagnosticTests: DiagnosticTest[] = [ + { + name: 'config-validation', + description: 'Validate transparency configuration', + run: async () => { + // Check if config is valid + return true; + }, + }, + { + name: 'telemetry-connection', + description: 'Check telemetry connectivity', + run: async () => { + // Simulate connectivity check + await new Promise((resolve) => setTimeout(resolve, 100)); + return true; + }, + }, + { + name: 'data-integrity', + description: 'Verify data integrity', + run: () => { + // Perform integrity checks + const dataValid = true; + return dataValid; + }, + }, + ]; + + const results = await runDiagnostics(diagnosticTests); + + const passed = results.filter((r: { status: string }) => r.status === 'pass').length; + logger.info(`Diagnostics completed: ${passed}/${results.length} passed`); + + return results; +} + +/** + * Example 3: Simulation scenarios for transparency testing + */ +export async function exampleTransparencySimulations() { + logger.info('Running transparency simulations...'); + + const scenarios: SimulationScenario< + { events: number }, + { processed: number; transparent: boolean } + >[] = [ + { + name: 'low-load-scenario', + description: 'Simulate low event load', + input: { events: 10 }, + run: async (input) => { + // Simulate processing events + await new Promise((resolve) => setTimeout(resolve, 50)); + return { + processed: input.events, + transparent: true, + }; + }, + validate: (output) => { + return output.processed > 0 && output.transparent === true; + }, + }, + { + name: 'high-load-scenario', + description: 'Simulate high event load', + input: { events: 1000 }, + run: async (input: { events: number }) => { + // Simulate processing many events + await new Promise((resolve) => setTimeout(resolve, 100)); + return { + processed: input.events, + transparent: input.events < 10000, // Transparency degrades under extreme load + }; + }, + validate: (output: { processed: number; transparent: boolean }) => { + return output.processed === 1000 && output.transparent === true; + }, + }, + { + name: 'error-handling-scenario', + description: 'Simulate error conditions', + input: { events: 5 }, + run: async (input: { events: number }) => { + // Simulate error handling + if (input.events < 10) { + return { + processed: input.events, + transparent: true, + }; + } + throw new Error('Too many events'); + }, + validate: (output: { processed: number; transparent: boolean }) => { + return output.transparent === true; + }, + }, + ]; + + const results = await runSimulations(scenarios); + + const successful = results.filter((r: { success: boolean }) => r.success).length; + logger.info(`Simulations completed: ${successful}/${results.length} successful`); + + return results; +} + +/** + * Run all examples + */ +export async function runAllExamples() { + logger.info('=== Running All Sandbox Examples ==='); + + try { + // Example 1: Basic sandbox + await exampleBasicSandbox(); + + // Example 2: Diagnostics + await exampleTransparencyDiagnostics(); + + // Example 3: Simulations + await exampleTransparencySimulations(); + + logger.info('=== All Examples Completed Successfully ==='); + } catch (error) { + logger.error('Examples failed', error instanceof Error ? error : undefined); + throw error; + } +} diff --git a/src/sandbox/index.ts b/src/sandbox/index.ts new file mode 100644 index 0000000..d5bb334 --- /dev/null +++ b/src/sandbox/index.ts @@ -0,0 +1,169 @@ +/** + * Sandbox module for running diagnostics and simulations + * Provides isolated execution environments for transparency testing + */ + +import { createLogger } from '../telemetry'; + +const logger = createLogger('sandbox'); + +/** + * Sandbox configuration options + */ +export interface SandboxOptions { + name: string; + timeout?: number; // in milliseconds + logLevel?: 'debug' | 'info' | 'warn' | 'error'; + isolated?: boolean; // whether to run in isolated context +} + +/** + * Result of a sandbox execution + */ +export interface SandboxResult { + success: boolean; + data?: T; + error?: Error; + duration: number; + logs: string[]; +} + +/** + * Sandbox class for isolated execution + */ +export class Sandbox { + private name: string; + private timeout: number; + private logLevel: string; + private isolated: boolean; + private logs: string[] = []; + + constructor(options: SandboxOptions) { + this.name = options.name; + this.timeout = options.timeout || 30000; // 30s default + this.logLevel = options.logLevel || 'info'; + this.isolated = options.isolated ?? true; + + logger.info(`Sandbox "${this.name}" initialized`, { + timeout: this.timeout, + isolated: this.isolated, + }); + } + + /** + * Log a message within the sandbox + */ + private log(level: string, message: string, ...args: unknown[]): void { + const logEntry = `[${level.toUpperCase()}] ${message} ${args.length > 0 ? JSON.stringify(args) : ''}`; + this.logs.push(logEntry); + + // Only log to telemetry logger if it should be shown based on log level + if (this.shouldLog(level)) { + try { + switch (level) { + case 'debug': + logger.debug(message); + break; + case 'info': + logger.info(message); + break; + case 'warn': + logger.warn(message); + break; + case 'error': + logger.error(message); + break; + } + } catch { + // Ignore logging errors in sandbox + } + } + } + + /** + * Check if a log level should be output + */ + private shouldLog(level: string): boolean { + const levels = ['debug', 'info', 'warn', 'error']; + const currentLevel = levels.indexOf(this.logLevel); + const messageLevel = levels.indexOf(level); + return messageLevel >= currentLevel; + } + + /** + * Execute a function within the sandbox + */ + async execute(fn: () => Promise | T): Promise> { + const startTime = Date.now(); + this.logs = []; // Reset logs for this execution + + this.log('info', `Starting sandbox execution: ${this.name}`); + + let timeoutId: NodeJS.Timeout | undefined; + + try { + // Create a timeout promise with cleanup + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Sandbox execution timeout after ${this.timeout}ms`)); + }, this.timeout); + }); + + // Execute the function with timeout + const result = await Promise.race([Promise.resolve(fn()), timeoutPromise]); + + const duration = Date.now() - startTime; + this.log('info', `Sandbox execution completed successfully`, { duration }); + + return { + success: true, + data: result, + duration, + logs: [...this.logs], + }; + } catch (error) { + const duration = Date.now() - startTime; + const err = error instanceof Error ? error : new Error(String(error)); + + this.log('error', `Sandbox execution failed`, { error: err.message }); + + return { + success: false, + error: err, + duration, + logs: [...this.logs], + }; + } finally { + // Clean up timeout to prevent memory leaks + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + } + } + + /** + * Get sandbox name + */ + getName(): string { + return this.name; + } + + /** + * Get current logs + */ + getLogs(): string[] { + return [...this.logs]; + } +} + +/** + * Create and execute a sandbox with a single function + */ +export async function runInSandbox( + name: string, + fn: () => Promise | T, + options?: Partial +): Promise> { + const sandbox = new Sandbox({ name, ...options }); + return sandbox.execute(fn); +} diff --git a/src/sandbox/simulation.ts b/src/sandbox/simulation.ts new file mode 100644 index 0000000..fc43e9c --- /dev/null +++ b/src/sandbox/simulation.ts @@ -0,0 +1,152 @@ +/** + * Simulation runner for scenario testing + */ + +import { Sandbox, SandboxResult } from './index'; +import { createLogger } from '../telemetry'; + +const logger = createLogger('simulation'); + +/** + * Simulation scenario configuration + */ +export interface SimulationScenario { + name: string; + description: string; + input: TInput; + run: (input: TInput) => Promise | TOutput; + validate?: (output: TOutput) => boolean; +} + +/** + * Simulation result + */ +export interface SimulationResult { + scenarioName: string; + success: boolean; + output?: TOutput; + error?: Error; + duration: number; + validated?: boolean; + logs: string[]; +} + +/** + * Simulation runner for executing scenario tests + */ +export class SimulationRunner { + private scenarios: SimulationScenario[] = []; + private sandbox: Sandbox; + + constructor(name: string = 'simulation-runner') { + this.sandbox = new Sandbox({ + name, + timeout: 120000, // 2 minutes for simulations + logLevel: 'info', + }); + } + + /** + * Register a simulation scenario + */ + registerScenario(scenario: SimulationScenario): void { + this.scenarios.push(scenario as SimulationScenario); + logger.debug(`Registered simulation scenario: ${scenario.name}`); + } + + /** + * Run all registered scenarios + */ + async runAll(): Promise { + logger.info(`Running ${this.scenarios.length} simulation scenarios`); + const results: SimulationResult[] = []; + + for (const scenario of this.scenarios) { + const result = await this.runScenario(scenario); + results.push(result); + } + + const successful = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + + logger.info('Simulation scenarios completed', { + total: results.length, + successful, + failed, + }); + + return results; + } + + /** + * Run a single simulation scenario + */ + async runScenario( + scenario: SimulationScenario + ): Promise> { + logger.info(`Running scenario: ${scenario.name}`); + + const result: SandboxResult = await this.sandbox.execute(async () => { + return await scenario.run(scenario.input); + }); + + // Validate output if validator is provided + let validated: boolean | undefined; + if (result.success && result.data !== undefined && scenario.validate) { + try { + validated = scenario.validate(result.data); + } catch (error) { + logger.warn(`Validation failed for scenario: ${scenario.name}`, { + error: error instanceof Error ? error.message : error, + }); + validated = false; + } + } + + return { + scenarioName: scenario.name, + success: this.determineSuccess(result.success, validated), + output: result.data, + error: result.error, + duration: result.duration, + validated, + logs: result.logs, + }; + } + + /** + * Determine overall success based on execution and validation results + */ + private determineSuccess(executionSuccess: boolean, validated: boolean | undefined): boolean { + // If execution failed, scenario failed + if (!executionSuccess) { + return false; + } + + // If no validation was performed, success is based on execution only + if (validated === undefined) { + return true; + } + + // If validation was performed, it must pass + return validated === true; + } + + /** + * Get number of registered scenarios + */ + getScenarioCount(): number { + return this.scenarios.length; + } +} + +/** + * Helper function to create and run simulations + */ +export async function runSimulations( + scenarios: SimulationScenario[] +): Promise { + const runner = new SimulationRunner(); + scenarios.forEach((scenario) => runner.registerScenario(scenario)); + return runner.runAll(); +} diff --git a/test/sandbox.test.js b/test/sandbox.test.js new file mode 100644 index 0000000..91500a8 --- /dev/null +++ b/test/sandbox.test.js @@ -0,0 +1,211 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { Sandbox, runInSandbox } from '../dist/sandbox/index.js'; +import { DiagnosticRunner, runDiagnostics } from '../dist/sandbox/diagnostics.js'; +import { SimulationRunner, runSimulations } from '../dist/sandbox/simulation.js'; + +test('Sandbox - basic execution', async () => { + const sandbox = new Sandbox({ name: 'test-sandbox' }); + + const result = await sandbox.execute(() => { + return 42; + }); + + assert.strictEqual(result.success, true); + assert.strictEqual(result.data, 42); + assert.ok(result.duration >= 0); + assert.ok(Array.isArray(result.logs)); +}); + +test('Sandbox - async execution', async () => { + const sandbox = new Sandbox({ name: 'async-sandbox' }); + + const result = await sandbox.execute(async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return 'async-result'; + }); + + assert.strictEqual(result.success, true); + assert.strictEqual(result.data, 'async-result'); +}); + +test('Sandbox - timeout handling', async () => { + const sandbox = new Sandbox({ name: 'timeout-sandbox', timeout: 50 }); + + const result = await sandbox.execute(async () => { + await new Promise((resolve) => setTimeout(resolve, 200)); + return 'should-timeout'; + }); + + assert.strictEqual(result.success, false); + assert.ok(result.error); + assert.match(result.error.message, /timeout/i); +}); + +test('Sandbox - error handling', async () => { + const sandbox = new Sandbox({ name: 'error-sandbox' }); + + const result = await sandbox.execute(() => { + throw new Error('Test error'); + }); + + assert.strictEqual(result.success, false); + assert.ok(result.error); + assert.strictEqual(result.error.message, 'Test error'); +}); + +test('runInSandbox helper', async () => { + const result = await runInSandbox('helper-test', () => { + return { value: 123 }; + }); + + assert.strictEqual(result.success, true); + assert.deepStrictEqual(result.data, { value: 123 }); +}); + +test('DiagnosticRunner - run passing tests', async () => { + const runner = new DiagnosticRunner('test-diagnostics'); + + runner.registerTest({ + name: 'test-1', + description: 'Should pass', + run: () => true, + }); + + runner.registerTest({ + name: 'test-2', + description: 'Should also pass', + run: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + return true; + }, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].status, 'pass'); + assert.strictEqual(results[1].status, 'pass'); +}); + +test('DiagnosticRunner - run failing tests', async () => { + const runner = new DiagnosticRunner('fail-diagnostics'); + + runner.registerTest({ + name: 'failing-test', + description: 'Should fail', + run: () => false, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].status, 'fail'); +}); + +test('DiagnosticRunner - skip tests', async () => { + const runner = new DiagnosticRunner('skip-diagnostics'); + + runner.registerTest({ + name: 'skipped-test', + description: 'Should skip', + run: () => true, + skip: true, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].status, 'skip'); +}); + +test('runDiagnostics helper', async () => { + const results = await runDiagnostics([ + { + name: 'diagnostic-1', + description: 'Test diagnostic', + run: () => true, + }, + ]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].status, 'pass'); +}); + +test('SimulationRunner - basic scenario', async () => { + const runner = new SimulationRunner('test-simulation'); + + runner.registerScenario({ + name: 'scenario-1', + description: 'Test scenario', + input: { value: 10 }, + run: (input) => { + return { result: input.value * 2 }; + }, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].success, true); + assert.deepStrictEqual(results[0].output, { result: 20 }); +}); + +test('SimulationRunner - scenario with validation', async () => { + const runner = new SimulationRunner('validation-simulation'); + + runner.registerScenario({ + name: 'validated-scenario', + description: 'Scenario with validation', + input: { value: 5 }, + run: (input) => { + return { result: input.value + 5 }; + }, + validate: (output) => { + return output.result === 10; + }, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].success, true); + assert.strictEqual(results[0].validated, true); +}); + +test('SimulationRunner - failing validation', async () => { + const runner = new SimulationRunner('fail-validation-simulation'); + + runner.registerScenario({ + name: 'invalid-scenario', + description: 'Scenario with failing validation', + input: { value: 5 }, + run: (input) => { + return { result: input.value + 5 }; + }, + validate: (output) => { + return output.result === 999; // This will fail + }, + }); + + const results = await runner.runAll(); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].success, false); + assert.strictEqual(results[0].validated, false); +}); + +test('runSimulations helper', async () => { + const results = await runSimulations([ + { + name: 'sim-1', + description: 'Test simulation', + input: 'test', + run: (input) => input.toUpperCase(), + }, + ]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].success, true); + assert.strictEqual(results[0].output, 'TEST'); +});