Add comprehensive integration tests for family training mode

Created 21 test cases covering:

- Variation tree building

- Position navigation

- Move validation across variations

- Branch point detection

- End of line detection

All tests pass (21/21).
This commit is contained in:
Stefan
2026-01-04 11:19:30 +01:00
parent bc5bc4a357
commit 039637b2da
@@ -0,0 +1,380 @@
/**
* Integration tests for Family Training Mode
* Tests the variation tree and multi-variation support
*/
import {
buildVariationTree,
getTreeNodeAtPosition,
getMatchingVariations,
getAllPossibleNextMoves,
isMoveInVariationTree,
identifyCurrentVariation,
describeCurrentPosition,
} from '../gameLogic';
import { OpeningMetadata } from '@/lib/openings';
import { MoveHistoryEntry } from '@/types/openingTraining';
// ============================================================================
// Test Data - Italian Game variations
// ============================================================================
const italianGameVariations: OpeningMetadata[] = [
{
eco: 'C50',
name: 'Italian Game',
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4',
src: 'test',
isEcoRoot: true,
},
{
eco: 'C53',
name: 'Italian Game: Classical Variation',
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3',
src: 'test',
},
{
eco: 'C54',
name: 'Italian Game: Giuoco Piano',
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4',
src: 'test',
},
{
eco: 'C55',
name: 'Italian Game: Two Knights Defense',
moves: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6',
src: 'test',
},
];
function createMoveEntry(
san: string,
color: 'white' | 'black',
moveNumber: number
): MoveHistoryEntry {
return {
moveNumber,
color,
san,
uci: 'e2e4',
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
evaluation: { score: 0, mate: null, depth: 15, bestMove: 'e4', ponder: null },
classification: {
category: 'in-theory',
inRepertoire: true,
evaluationChange: 0,
isSignificantSwing: false,
theoreticalAlternatives: [],
},
};
}
// ============================================================================
// Tests
// ============================================================================
describe('Family Training Mode - Variation Tree', () => {
describe('buildVariationTree', () => {
it('should build a tree from multiple variations', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
expect(tree.familyName).toBe('Italian Game');
expect(tree.allVariations).toHaveLength(4);
expect(tree.children.size).toBe(1); // Only 1. e4 at root
expect(tree.children.has('e4')).toBe(true);
});
it('should create correct branching structure', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4, there should be 2 branches
const e4Node = tree.children.get('e4')!;
const e5Node = e4Node.children.get('e5')!;
const nf3Node = e5Node.children.get('Nf3')!;
const nc6Node = nf3Node.children.get('Nc6')!;
const bc4Node = nc6Node.children.get('Bc4')!;
// After Bc4, should have Bc5 and Nf6 as options
expect(bc4Node.children.size).toBe(2);
expect(bc4Node.children.has('Bc5')).toBe(true);
expect(bc4Node.children.has('Nf6')).toBe(true);
});
it('should track variations at each node', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const e4Node = tree.children.get('e4')!;
// All variations start with e4
expect(e4Node.variations).toHaveLength(4);
});
it('should mark end of line correctly', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// Navigate to end of "Italian Game" (shortest variation)
const e4Node = tree.children.get('e4')!;
const e5Node = e4Node.children.get('e5')!;
const nf3Node = e5Node.children.get('Nf3')!;
const nc6Node = nf3Node.children.get('Nc6')!;
const bc4Node = nc6Node.children.get('Bc4')!;
expect(bc4Node.isEndOfLine).toBe(true); // End of C50 variation
});
});
describe('getTreeNodeAtPosition', () => {
it('should return root node for empty move history', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const node = getTreeNodeAtPosition(tree, []);
expect(node).not.toBeNull();
expect(node!.variations).toHaveLength(4);
});
it('should navigate to correct position', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
];
const node = getTreeNodeAtPosition(tree, moveHistory);
expect(node).not.toBeNull();
expect(node!.move).toBe('Nf3');
});
it('should return null for moves not in any variation', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moveHistory = [
createMoveEntry('d4', 'white', 1), // Not in Italian Game!
];
const node = getTreeNodeAtPosition(tree, moveHistory);
expect(node).toBeNull();
});
});
describe('getMatchingVariations', () => {
it('should return all variations at start', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const variations = getMatchingVariations(tree, []);
expect(variations).toHaveLength(4);
});
it('should narrow down variations as moves are played', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
createMoveEntry('Nf6', 'black', 3),
];
const variations = getMatchingVariations(tree, moveHistory);
// Only Two Knights Defense
expect(variations).toHaveLength(1);
expect(variations[0].name).toBe('Italian Game: Two Knights Defense');
});
it('should return empty array for off-book moves', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('c5', 'black', 1), // Sicilian, not Italian!
];
const variations = getMatchingVariations(tree, moveHistory);
expect(variations).toHaveLength(0);
});
});
describe('getAllPossibleNextMoves', () => {
it('should return all possible first moves', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moves = getAllPossibleNextMoves(tree, []);
expect(moves).toHaveLength(1);
expect(moves[0].move).toBe('e4');
expect(moves[0].variations).toHaveLength(4);
});
it('should return multiple options at branch points', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// After 1. e4 e5 2. Nf3 Nc6 3. Bc4
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
];
const moves = getAllPossibleNextMoves(tree, moveHistory);
// Should have Bc5 (Classical/Giuoco Piano) and Nf6 (Two Knights)
expect(moves).toHaveLength(2);
const moveNames = moves.map(m => m.move).sort();
expect(moveNames).toEqual(['Bc5', 'Nf6']);
});
it('should return empty array at end of all variations', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// Go to end of longest variation (Giuoco Piano)
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
createMoveEntry('Bc5', 'black', 3),
createMoveEntry('c3', 'white', 4),
createMoveEntry('Nf6', 'black', 4),
createMoveEntry('d4', 'white', 5),
];
const moves = getAllPossibleNextMoves(tree, moveHistory);
expect(moves).toHaveLength(0);
});
});
describe('isMoveInVariationTree', () => {
it('should return true for valid moves', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const isValid = isMoveInVariationTree(tree, [], 'e4');
expect(isValid).toBe(true);
});
it('should return false for invalid moves', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const isValid = isMoveInVariationTree(tree, [], 'd4');
expect(isValid).toBe(false);
});
it('should validate moves at branch points', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
];
expect(isMoveInVariationTree(tree, moveHistory, 'Bc5')).toBe(true);
expect(isMoveInVariationTree(tree, moveHistory, 'Nf6')).toBe(true);
expect(isMoveInVariationTree(tree, moveHistory, 'd6')).toBe(false);
});
});
describe('describeCurrentPosition', () => {
it('should describe starting position', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const desc = describeCurrentPosition(tree, []);
expect(desc.matchingCount).toBe(4);
expect(desc.isEndOfLine).toBe(false);
expect(desc.nextMoves).toContain('e4');
});
it('should identify branch points', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
];
const desc = describeCurrentPosition(tree, moveHistory);
expect(desc.matchingCount).toBe(4); // All 4 variations pass through this position
expect(desc.nextMoves).toHaveLength(2);
expect(desc.nextMoves).toContain('Bc5');
expect(desc.nextMoves).toContain('Nf6');
});
it('should detect end of line', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// Go to actual end of a line (Giuoco Piano)
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
createMoveEntry('Bc5', 'black', 3),
createMoveEntry('c3', 'white', 4),
createMoveEntry('Nf6', 'black', 4),
createMoveEntry('d4', 'white', 5),
];
const desc = describeCurrentPosition(tree, moveHistory);
expect(desc.isEndOfLine).toBe(true); // End of Giuoco Piano
expect(desc.matchingCount).toBe(1); // Only Giuoco Piano reaches here
expect(desc.nextMoves).toHaveLength(0); // No more moves in repertoire
});
});
describe('identifyCurrentVariation', () => {
it('should identify specific variation', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// Two Knights Defense
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
createMoveEntry('Nf6', 'black', 3),
];
const result = identifyCurrentVariation(tree, moveHistory);
expect(result.exact).toHaveLength(1);
expect(result.exact[0].name).toBe('Italian Game: Two Knights Defense');
});
it('should return multiple variations when moves overlap', () => {
const tree = buildVariationTree(italianGameVariations, 'Italian Game');
// After Bc5, both Classical and Giuoco Piano are possible
const moveHistory = [
createMoveEntry('e4', 'white', 1),
createMoveEntry('e5', 'black', 1),
createMoveEntry('Nf3', 'white', 2),
createMoveEntry('Nc6', 'black', 2),
createMoveEntry('Bc4', 'white', 3),
createMoveEntry('Bc5', 'black', 3),
];
const result = identifyCurrentVariation(tree, moveHistory);
// No exact matches at this position, but multiple possible continuations
expect(result.possible.length).toBeGreaterThanOrEqual(2);
const names = result.possible.map(v => v.name);
expect(names).toContain('Italian Game: Classical Variation');
expect(names).toContain('Italian Game: Giuoco Piano');
});
});
});