import { describe, it, expect, beforeEach } from 'vitest'; import { Chess } from 'chess.js'; import { handleCommit } from '../../src/commit.js'; import type { Game } from '../../src/state.js'; import { RATE_LIMIT } from '../../src/state.js'; function makeGame(fen?: string): Game { const chess = fen ? new Chess(fen) : new Chess(); return { id: 'testtest', mode: 'blind', highlightingEnabled: false, status: 'active', createdAt: Date.now(), chess, moveHistory: [], announcements: [], players: { w: { token: 'w'.repeat(24), socket: null, joinedAt: 0, rateBucket: { tokens: RATE_LIMIT.capacity, last: 0 } }, b: { token: 'b'.repeat(24), socket: null, joinedAt: 0, rateBucket: { tokens: RATE_LIMIT.capacity, last: 0 } }, }, armed: null, drawOffer: null, disconnectAt: {}, }; } describe('hierarchy decision table', () => { let game: Game; beforeEach(() => { game = makeGame(); }); it('row 1: no_such_piece — empty square', () => { const r = handleCommit(game, 'w', { from: 'e4' }); expect(r.kind).toBe('announce'); if (r.kind === 'announce') expect(r.announcements[0]!.text).toBe('no_such_piece'); }); it('row 1b: no_such_piece — opponent piece', () => { const r = handleCommit(game, 'w', { from: 'e7' }); // black pawn expect(r.kind).toBe('announce'); if (r.kind === 'announce') expect(r.announcements[0]!.text).toBe('no_such_piece'); }); it('row 2: no_legal_moves — knight in starting position is OK; use a contrived fen', () => { // White knight surrounded by own pieces. Place a knight at b1 with own // pieces blocking a3, c3, d2. const g = makeGame('4k3/8/8/8/8/P1P5/3P4/1N2K3 w - - 0 1'); // Knight at b1: jumps a3, c3, d2. All blocked by own pawns. const r = handleCommit(g, 'w', { from: 'b1' }); expect(r.kind).toBe('announce'); if (r.kind === 'announce') expect(r.announcements[0]!.text).toBe('no_legal_moves'); }); it('row 3: wont_help — pinned bishop, not in check', () => { // White king on e1, white bishop on e2 pinned by black rook on e8. const g = makeGame('4r2k/8/8/8/8/8/4B3/4K3 w - - 0 1'); const r = handleCommit(g, 'w', { from: 'e2' }); expect(r.kind).toBe('announce'); if (r.kind === 'announce') expect(r.announcements[0]!.text).toBe('wont_help'); }); it('row 5: silent on first commit when piece has legal moves', () => { const r = handleCommit(game, 'w', { from: 'e2' }); expect(r.kind).toBe('silent'); expect(game.armed).toEqual({ color: 'w', from: 'e2' }); }); it('row 6: applied move when legal commit completes', () => { const r = handleCommit(game, 'w', { from: 'e2', to: 'e4' }); expect(r.kind).toBe('applied'); expect(game.armed).toBeNull(); expect(game.chess.history()).toContain('e4'); }); }); describe('touch-move enforcement', () => { it('after silent arm, commit with different from yields must_move_touched_piece', () => { const game = makeGame(); handleCommit(game, 'w', { from: 'e2' }); // arm const r = handleCommit(game, 'w', { from: 'd2', to: 'd4' }); expect(r.kind).toBe('error'); if (r.kind === 'error') expect(r.code).toBe('must_move_touched_piece'); }); it('after silent arm, illegal destination returns illegal_move and KEEPS touch', () => { const game = makeGame(); handleCommit(game, 'w', { from: 'e2' }); const r = handleCommit(game, 'w', { from: 'e2', to: 'e5' }); // pawn can't go e2→e5 expect(r.kind).toBe('announce'); if (r.kind === 'announce') expect(r.announcements[0]!.text).toBe('illegal_move'); expect(game.armed).toEqual({ color: 'w', from: 'e2' }); }); it('not your turn returns error', () => { const game = makeGame(); const r = handleCommit(game, 'b', { from: 'e7' }); expect(r.kind).toBe('error'); if (r.kind === 'error') expect(r.code).toBe('not_your_turn'); }); }); describe('promotion', () => { it('promotion required: pawn on 7th to 8th without promotion field', () => { const g = makeGame('7k/4P3/8/8/8/8/8/4K3 w - - 0 1'); const r = handleCommit(g, 'w', { from: 'e7', to: 'e8' }); expect(r.kind).toBe('error'); if (r.kind === 'error') expect(r.code).toBe('promotion_required'); }); it('promotion succeeds with field', () => { const g = makeGame('7k/4P3/8/8/8/8/8/4K3 w - - 0 1'); const r = handleCommit(g, 'w', { from: 'e7', to: 'e8', promotion: 'q' }); expect(r.kind).toBe('applied'); if (r.kind === 'applied') { const promo = r.announcements.find((a) => a.text === 'white_promoted'); expect(promo).toBeDefined(); expect(promo?.payload?.promotedTo).toBe('q'); } }); });