Fourteen tests already guard the happy paths, because each lesson shipped its own. This lesson writes the other kind: the fixtures that live where chess actually breaks software — state that doesn’t round-trip, endings that aren’t checkmate, positions that lie. No production code changes at all; by the end, the Rust suite reads as a specification. Checkpoint: lesson-10.

Round trips are where saves go to die

Lesson 9’s persistence rests entirely on one claim: get_fentry_set_fen loses nothing. Three tests attack it. The first saves after

  1. e4 and asserts the string itself carries the turn and castling rights before restoring:
let saved = g.get_fen();
assert!(saved.contains(" b "), "turn must be saved: {saved}");
assert!(saved.contains("KQkq"), "castling rights must be saved: {saved}");

The second proves castling is still playable after a round trip — not just that the rights field looks right, but that O-O actually executes in the restored game. The third is the one that catches real bugs in real chess apps: set up a position where en passant is legal, round-trip it, and capture en passant in the restored game. If get_fen used EnPassantMode::Always or the restore path dropped the field, this test is the only thing that would tell you. A save format tested only by “restore then compare” can still lose rights the comparison doesn’t exercise; these assert on behavior.

Endings that aren’t checkmate

Fool’s mate — f3 e5 g4 Qh4 — gives the suite a real checkmate in four plies: game over, check flagged, 0-1, and both the opponent and try_make_move refusing to continue. Its counterpart is the ending beginners’ engines get wrong:

g.try_set_fen("k7/2Q5/1K6/8/8/8/8/8 b - - 0 1").unwrap();
assert!(g.is_game_over());
assert!(!g.is_check());
assert_eq!(g.get_result(), "1/2-1/2");

Black has no legal move and is not in check: stalemate, a draw, half the point gone. The !is_check() assert is the line doing the work — it’s what distinguishes this ending from mate for any code that consumes get_result.

The last fixture feeds try_set_fen a board with two white kings. Syntactically valid FEN, impossible chess — and it’s rejected by shakmaty’s into_position stage, not the string parser. That’s the lesson-3 two-stage design proving it was two stages for a reason.

What nineteen tests cost and buy

Run the suite and watch the whole thing finish in a few milliseconds — cargo test compiles to native code via the rlib target, so there’s no wasm, no browser, no fixture server anywhere. That speed is a design outcome, and it’s worth stating the causal chain plainly: the boundary keeps JS types out of the core, the core stays natively compilable, so the tests cost nothing to run and you run them constantly.

If you want a number for coverage, cargo llvm-cov reports on this crate nicely (install cargo-llvm-cov first; the repo wires it as coverage:rust in a later lesson). But the more useful audit is reading the test names top to bottom and checking they describe the rules of the crate rather than its implementation — that’s what makes a suite double as documentation.

Build it

File Action What goes in it
chess-engine/src/lib.rs modify Five new tests (full_fen_round_trip_preserves_state, castling_rights_survive_round_trip, en_passant_survives_round_trip, cpu_has_no_moves_after_checkmate, stalemate_is_a_draw) plus the two-kings assert added to rejects_invalid_fen

Done when: npm run test:rust reports 19 passed.

Answer key: lesson-09...lesson-10.

Challenge: the draw the engine can’t see

Threefold repetition and the fifty-move rule are draws too. Write a test shuffling knights back and forth and discover what is_game_over says — then work out why from shakmaty’s docs: a Chess position doesn’t carry its own history, so repetition is unknowable from inside it. Decide where that responsibility would have to live in this architecture, and write your finding as a comment on the test.

Next lesson the tests cross the boundary: the actual component, running the actual wasm, in a DOM that isn’t a browser.