v0.13.0 web and terminal client enhance, replay api, linux server deploy, doc and dependency update
test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-28 22:35:52 -04:00
1 parent 52ac0fe31a
commit f1ac4cbbe7
71 files changed
+7677 -537

No files matched your search

+156
View File
@@ -0,0 +1,156 @@
package service
import (
"errors"
"strings"
"testing"
"chess/internal/server/chess"
"chess/internal/server/core"
"github.com/google/uuid"
)
// playLine creates a game and commits uci as the players' moves, with FENs
// from the rules core and the given final state (reached by checkmate) on the
// last move.
func playLine(t *testing.T, svc *Service, userID string, black core.PlayerConfig, uci []string, final core.State) string {
t.Helper()
gameID := uuid.NewString()
white := core.NewPlayer(core.PlayerConfig{Type: core.PlayerHuman}, core.ColorWhite)
blackPlayer := core.NewPlayer(black, core.ColorBlack)
if err := svc.CreateGame(gameID, white, blackPlayer, chess.StartFEN, core.ColorWhite, core.StateOngoing, core.TermNone); err != nil {
t.Fatal(err)
}
pos, err := chess.ParseFEN(chess.StartFEN)
if err != nil {
t.Fatal(err)
}
for i, text := range uci {
m, err := pos.ParseUCI(text)
if err != nil {
t.Fatal(err)
}
next := pos.Play(m)
state, termination := core.StateOngoing, core.TermNone
if i == len(uci)-1 && final.IsTerminal() {
state, termination = final, core.TermCheckmate
}
turn, actor := core.ColorWhite, userID
if i%2 == 1 {
turn, actor = core.ColorBlack, ""
}
if err := svc.ApplyMoveWithState(gameID, MoveCommit{
ExpectedFEN: pos.FEN(), ExpectedState: core.StateOngoing, ExpectedTurn: turn,
ActorUserID: actor, MoveUCI: text, NewFEN: next.FEN(), State: state, Termination: termination,
}); err != nil {
t.Fatal(err)
}
pos = next
}
return gameID
}
func TestHistoryNotationAndPGN(t *testing.T) {
svc := newPersistentTestService(t)
computer := core.PlayerConfig{Type: core.PlayerComputer, Level: 3, SearchTime: 100}
gameID := playLine(t, svc, uuid.NewString(), computer,
[]string{"f2f3", "e7e5", "g2g4", "d8h4"}, core.StateBlackWins)
history, err := svc.GetGameHistory(gameID)
if err != nil {
t.Fatal(err)
}
var san []string
for _, m := range history.Moves {
san = append(san, m.SAN)
}
if strings.Join(san, " ") != "f3 e5 g4 Qh4#" || history.PGNResult != "0-1" || history.Termination != "checkmate" {
t.Fatalf("history notation = %v, %q, %q", san, history.PGNResult, history.Termination)
}
pgn, err := svc.GetGamePGN(gameID, -1)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"[Event \"Casual game\"]\n[Site \"?\"]\n", "[White \"Anonymous\"]\n", "[Black \"Stockfish level 3\"]\n",
"[Result \"0-1\"]\n", "[GameId \"" + gameID + "\"]\n", "[BlackType \"program\"]\n", "[PlyCount \"4\"]\n",
"[Termination \"normal\"]\n", "\n\n1. f3 e5 2. g4 Qh4# { Black wins by checkmate. } 0-1\n",
} {
if !strings.Contains(pgn.Text, want) {
t.Errorf("PGN lacks %q:\n%s", want, pgn.Text)
}
}
if !strings.HasPrefix(pgn.Filename, "chess-") || !strings.HasSuffix(pgn.Filename, "-"+gameID[:8]+".pgn") {
t.Errorf("filename = %q", pgn.Filename)
}
partial, err := svc.GetGamePGN(gameID, 2)
if err != nil {
t.Fatal(err)
}
if !strings.HasSuffix(partial.Text, "\n\n1. f3 e5 *\n") || strings.Contains(partial.Text, "Termination") ||
!strings.HasSuffix(partial.Filename, "-ply2.pgn") {
t.Errorf("partial PGN %q:\n%s", partial.Filename, partial.Text)
}
if full, err := svc.GetGamePGN(gameID, 4); err != nil || full.Text != pgn.Text {
t.Errorf("ply equal to the line length should export the whole game: %v", err)
}
if _, err := svc.GetGamePGN(gameID, 5); !errors.Is(err, ErrPlyOutOfRange) {
t.Errorf("ply 5 error = %v, want ErrPlyOutOfRange", err)
}
if _, err := svc.GetGamePGN(uuid.NewString(), -1); !errors.Is(err, ErrGameNotFound) {
t.Errorf("unknown game error = %v, want ErrGameNotFound", err)
}
}
func TestUserGamesCursorAndFilters(t *testing.T) {
svc := newPersistentTestService(t)
userID := uuid.NewString()
human := core.PlayerConfig{Type: core.PlayerHuman}
var ids []string
for i := 0; i < 3; i++ {
ids = append([]string{playLine(t, svc, userID, human, []string{"e2e4"}, core.StateOngoing)}, ids...)
}
finished := playLine(t, svc, userID, human, []string{"f2f3", "e7e5", "g2g4", "d8h4"}, core.StateBlackWins)
ids = append([]string{finished}, ids...)
var got []string
cursor := ""
for {
page, err := svc.GetUserGames(userID, UserGamesOptions{Limit: 3, Cursor: cursor})
if err != nil {
t.Fatal(err)
}
if cursor != "" && page.NextOffset != nil {
t.Error("cursor page reported nextOffset")
}
for _, g := range page.Games {
got = append(got, g.GameID)
}
if page.NextCursor == "" {
break
}
cursor = page.NextCursor
}
if strings.Join(got, ",") != strings.Join(ids, ",") {
t.Fatalf("cursor pages = %v, want %v", got, ids)
}
page, err := svc.GetUserGames(userID, UserGamesOptions{Limit: 10, Status: "finished", Color: "white"})
if err != nil {
t.Fatal(err)
}
if len(page.Games) != 1 || page.Games[0].GameID != finished || page.Games[0].PGNResult != "0-1" {
t.Fatalf("finished games = %+v", page.Games)
}
for _, bad := range []UserGamesOptions{
{Limit: 1, Cursor: "!!"}, {Limit: 1, Cursor: "bm90LWEtY3Vyc29y"}, {Limit: 1, Offset: 1, Cursor: page.NextCursor + "x"},
{Limit: 1, Color: "red"}, {Limit: 1, Status: "paused"},
} {
if _, err := svc.GetUserGames(userID, bad); !errors.Is(err, ErrInvalidListQuery) {
t.Errorf("GetUserGames(%+v) error = %v, want ErrInvalidListQuery", bad, err)
}
}
}