Run and debug Midnight contract tests using Vitest simulators. Use this skill when testing contracts, debugging test failures, or writing new tests...
Run, debug, and write tests for Midnight smart contracts using Vitest and contract simulators.
Use this skill when:
# 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 -- --coverage
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 simulator
// 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);
});
});
// 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();
}
}
it('updates ledger state correctly', () => {
const before = simulator.getLedger();
simulator.increment();
const after = simulator.getLedger();
expect(after.round).toBe(before.round + 1n);
});
it('rejects invalid operations', () => {
expect(() => {
simulator.withdraw(1000n); // More than balance
}).toThrow('Insufficient balance');
});
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);
});
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();
});
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);
});
npm run test
npm run test -- counter.test.ts
npm run test -- --grep "increment"
npm run test:watch
npm run test -- --coverage
npm run test -- --reporter=verbose
it.only('focuses on this test', () => {
// Only this test runs
});
it.skip('skip this test temporarily', () => {
// Skipped
});
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));
});
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 optionsExamples:
# 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.ts
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.23s
Error: Cannot find module './simulators/simulator'
Solution: Create simulator file or check import path
Error: Type 'number' is not assignable to type 'bigint'
Solution: Use BigInt() or n suffix: 100n
Error: Test timeout exceeded
Solution: Increase timeout or check for unresolved promises:
it('async test', async () => {
await simulator.asyncOperation();
}, 10000); // 10 second timeout
Error: Cannot find compiled artifacts
Solution: Run npm run build before tests
beforeEach for fresh state