Run and debug Midnight contract tests using Vitest simulators. Use this skill when testing contracts, debugging test failures, or writing new tests. Triggers on "run tests", "test contract", "debug test", "test fails", or "vitest".
Resources
1Install
npx skillscat add uvroxx/midnight-agent-skills/midnight-test-runner Install via the SkillsCat registry.
SKILL.md
Midnight Test Runner
Run, debug, and write tests for Midnight smart contracts using Vitest and contract simulators.
When to Use
Use this skill when:
- Running contract test suites
- Debugging failing tests
- Writing new test cases
- Testing privacy features (selective disclosure)
- Validating ZK circuit behavior
How It Works
- Compiles Compact contract
- Creates contract simulator from compiled artifacts
- Runs Vitest test suite
- Reports results with coverage
Quick Start
# Navigate to contract directory
cd counter-contract
# Run all tests
npm run test
# Run with watch mode
npm run test:watch
# Run with coverage
npm run test -- --coverageTest Structure
Directory Layout
counter-contract/
├── src/
│ ├── counter.compact # Contract source
│ ├── witnesses.ts # Private state types
│ ├── managed/ # Compiled artifacts
│ └── test/
│ ├── counter.test.ts # Test file
│ └── simulators/
│ └── simulator.ts # Contract simulatorBasic Test File
// counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { CounterSimulator } from './simulators/simulator';
describe('Counter Contract', () => {
let simulator: CounterSimulator;
beforeEach(() => {
simulator = CounterSimulator.deployContract(0);
});
it('initializes with correct values', () => {
const ledger = simulator.getLedger();
expect(ledger.round).toBe(0n);
});
it('increments the counter', () => {
simulator.as('player1').increment();
const ledger = simulator.getLedger();
expect(ledger.round).toBe(1n);
});
});Contract Simulator Pattern
Creating a Simulator
// simulators/simulator.ts
import { Contract } from '../managed/contract';
type LedgerState = {
round: bigint;
};
type PrivateState = {
privateCounter: number;
};
export class CounterSimulator {
private ledger: LedgerState;
private privateStates: Map<string, PrivateState>;
private currentPlayer: string = 'default';
private constructor(initialValue: number) {
this.ledger = { round: BigInt(initialValue) };
this.privateStates = new Map();
this.privateStates.set('default', { privateCounter: initialValue });
}
static deployContract(initialValue: number): CounterSimulator {
return new CounterSimulator(initialValue);
}
as(playerId: string): CounterSimulator {
this.currentPlayer = playerId;
if (!this.privateStates.has(playerId)) {
this.privateStates.set(playerId, { privateCounter: 0 });
}
return this;
}
getLedger(): LedgerState {
return { ...this.ledger };
}
getPrivateState(): PrivateState {
return { ...this.privateStates.get(this.currentPlayer)! };
}
increment(): LedgerState {
this.ledger.round += 1n;
return this.getLedger();
}
}Testing Patterns
Testing State Changes
it('updates ledger state correctly', () => {
const before = simulator.getLedger();
simulator.increment();
const after = simulator.getLedger();
expect(after.round).toBe(before.round + 1n);
});Testing Assertions
it('rejects invalid operations', () => {
expect(() => {
simulator.withdraw(1000n); // More than balance
}).toThrow('Insufficient balance');
});Testing Private State
it('maintains separate private state per player', () => {
simulator.as('player1').setPrivateValue(100);
simulator.as('player2').setPrivateValue(200);
expect(simulator.as('player1').getPrivateState().value).toBe(100);
expect(simulator.as('player2').getPrivateState().value).toBe(200);
});Testing Selective Disclosure
it('proves balance threshold without revealing balance', () => {
// Set private balance
simulator.setPrivateBalance(50000n);
// Prove balance > 10000 (should succeed)
expect(() => {
simulator.proveBalanceAboveThreshold(10000n);
}).not.toThrow();
// Prove balance > 100000 (should fail)
expect(() => {
simulator.proveBalanceAboveThreshold(100000n);
}).toThrow('Balance below threshold');
// Verify ledger doesn't expose actual balance
const ledger = simulator.getLedger();
expect(ledger.actualBalance).toBeUndefined();
});Testing Multi-Player Scenarios
it('handles turn-based gameplay', () => {
// Player 1 commits move
simulator.as('player1').commitMove(hashMove(1, 'salt1'));
expect(simulator.getLedger().gameState).toBe(1);
// Player 2 commits move
simulator.as('player2').commitMove(hashMove(2, 'salt2'));
expect(simulator.getLedger().gameState).toBe(2);
// Reveal phase
simulator.revealMoves(1, 'salt1', 2, 'salt2');
expect(simulator.getLedger().winner).toBe(2);
});Running Tests
All Tests
npm run testSpecific File
npm run test -- counter.test.tsWith Pattern
npm run test -- --grep "increment"Watch Mode
npm run test:watchCoverage Report
npm run test -- --coverageDebugging Tests
Enable Verbose Output
npm run test -- --reporter=verboseDebug Single Test
it.only('focuses on this test', () => {
// Only this test runs
});Skip Failing Tests
it.skip('skip this test temporarily', () => {
// Skipped
});Console Debugging
it('debug with console', () => {
const ledger = simulator.getLedger();
console.log('Ledger state:', JSON.stringify(ledger, null, 2));
simulator.increment();
const after = simulator.getLedger();
console.log('After increment:', JSON.stringify(after, null, 2));
});Test Script
bash /path/to/skills/midnight-test-runner/scripts/test.sh [contract-path] [options]Arguments:
contract-path- Path to contract directory (default: current)options- Additional vitest options
Examples:
# Run all tests
bash scripts/test.sh ./counter-contract
# Run with coverage
bash scripts/test.sh ./counter-contract --coverage
# Run specific test file
bash scripts/test.sh ./counter-contract counter.test.tsPresent Results to User
Test Results:
PASS src/test/counter.test.ts (5 tests)
✓ initializes with correct values (2ms)
✓ increments the counter (1ms)
✓ maintains private state separately (3ms)
✓ rejects negative amounts (1ms)
✓ proves balance threshold (4ms)
Tests: 5 passed, 5 total
Time: 1.23sTroubleshooting
Tests Not Finding Simulator
Error: Cannot find module './simulators/simulator'Solution: Create simulator file or check import path
Type Errors in Tests
Error: Type 'number' is not assignable to type 'bigint'Solution: Use BigInt() or n suffix: 100n
Async Test Timeout
Error: Test timeout exceededSolution: Increase timeout or check for unresolved promises:
it('async test', async () => {
await simulator.asyncOperation();
}, 10000); // 10 second timeoutContract Not Compiled
Error: Cannot find compiled artifactsSolution: Run npm run build before tests
Best Practices
- Test edge cases - Empty arrays, zero values, max values
- Test assertions - Verify error messages match
- Test privacy - Ensure private data stays private
- Isolate tests - Use
beforeEachfor fresh state - Name clearly - Test names should describe expected behavior