refactor: move test-only helpers out of production code and drop stale comments

bot.BoardFor and hub.roomCount were reachable from tests only and now live beside them; session.serve drops an always-true nil check (readLoop never returns nil); invisible characters in test literals become escapes; comments no longer cite plan phases; `.dockerignore` keeps the dump out of the build context. No behaviour change; vet, deadcode, staticcheck, go test -race, svelte-check and vitest green.
This commit is contained in:
tiennm99 committed 2026-09-08 23:10:39 +07:00
1 parent f00d0ef974
commit e29c06d6f0
11 files changed
+64 -50

No files matched your search

+3
View File
@@ -8,6 +8,9 @@ web/build
web/.svelte-kit web/.svelte-kit
web/test-results web/test-results
web/playwright-report web/playwright-report
data/*.bz2
data/*.bz2.part
data/*.xml
data/*.db data/*.db
data/*.db-journal data/*.db-journal
data/*.db-wal data/*.db-wal
+1 -1
View File
@@ -59,7 +59,7 @@ func TestStripWikitext(t *testing.T) {
"Một nghĩa."}, "Một nghĩa."},
{"a stray closer is not text", "Một }} nghĩa.", "Một nghĩa."}, {"a stray closer is not text", "Một }} nghĩa.", "Một nghĩa."},
{"only punctuation is empty", "(...).", ""}, {"only punctuation is empty", "(...).", ""},
{"control characters and whitespace collapse", " Một từ \t hai ", "Một từ hai"}, {"control characters and whitespace collapse", " Một\a từ \t hai ", "Một từ hai"},
{"unclosed template does not leak", "Một {{label|vi|x từ.", "Một"}, {"unclosed template does not leak", "Một {{label|vi|x từ.", "Một"},
} }
for _, c := range cases { for _, c := range cases {
+36
View File
@@ -0,0 +1,36 @@
package bot
import (
"iter"
"github.com/tiennm99dev/noitu/server/internal/game"
)
// engineBoard adapts an Engine to Board for the simulations in this package.
// The server never needs it: the room hands the bot a Board of its own.
type engineBoard struct {
e *game.Engine
dict game.Dictionary
}
// BoardFor wraps an engine so a test can drive a strategy against it.
//
// The dictionary comes from the engine rather than the caller: handing in a
// different one would let the bot search a graph the engine does not validate
// against, and that failure surfaces as the engine rejecting its own bot's
// move at runtime.
func BoardFor(e *game.Engine) Board {
return &engineBoard{e: e, dict: e.Dict()}
}
func (b *engineBoard) LegalMoves() []string { return b.e.LegalMoves() }
func (b *engineBoard) Used(word string) bool { return b.e.Used(word) }
func (b *engineBoard) WordsStartingWith(syllable string) iter.Seq[string] {
return b.dict.WordsStartingWith(syllable)
}
func (b *engineBoard) LastSyllable(word string) (string, bool) {
return b.dict.LastSyllable(word)
}
-30
View File
@@ -14,8 +14,6 @@ import (
"iter" "iter"
"math/rand/v2" "math/rand/v2"
"time" "time"
"github.com/tiennm99dev/noitu/server/internal/game"
) )
// Difficulty selects a strategy. // Difficulty selects a strategy.
@@ -96,34 +94,6 @@ func thinkingDelay(rng *rand.Rand, min, max time.Duration) time.Duration {
return min + time.Duration(rng.Int64N(int64(max-min))) return min + time.Duration(rng.Int64N(int64(max-min)))
} }
// engineBoard adapts an Engine to Board.
type engineBoard struct {
e *game.Engine
dict game.Dictionary
}
// BoardFor wraps an engine so strategies can inspect it.
//
// The dictionary comes from the engine rather than the caller: handing in a
// different one would let the bot search a graph the engine does not validate
// against, and that failure surfaces as the engine rejecting its own bot's
// move at runtime.
func BoardFor(e *game.Engine) Board {
return &engineBoard{e: e, dict: e.Dict()}
}
func (b *engineBoard) LegalMoves() []string { return b.e.LegalMoves() }
func (b *engineBoard) Used(word string) bool { return b.e.Used(word) }
func (b *engineBoard) WordsStartingWith(syllable string) iter.Seq[string] {
return b.dict.WordsStartingWith(syllable)
}
func (b *engineBoard) LastSyllable(word string) (string, bool) {
return b.dict.LastSyllable(word)
}
// remainingOutDegree counts the continuations still available from a syllable, // remainingOutDegree counts the continuations still available from a syllable,
// ignoring `excluding` — the move being considered, which will itself be spent // ignoring `excluding` — the move being considered, which will itself be spent
// once played. Pass "" to exclude nothing. // once played. Pass "" to exclude nothing.
+2 -2
View File
@@ -1,6 +1,6 @@
// Package wsapi is the transport layer: it translates between the game engine // Package wsapi is the transport layer: it translates between the game engine
// and the generated protobuf wire types, and (from phase 5 on) owns the // and the generated protobuf wire types, and owns the WebSocket sessions and
// WebSocket sessions and rooms built on top of them. // rooms built on top of them.
// //
// The engine's reason enums and the wire enums are deliberately distinct // The engine's reason enums and the wire enums are deliberately distinct
// types. Renumbering an internal constant must never silently change what a // types. Renumbering an internal constant must never silently change what a
-8
View File
@@ -230,14 +230,6 @@ func (h *hub) shutdown() {
} }
} }
// roomCount reports live rooms. Tests use it to assert eviction; nothing in
// the server depends on it.
func (h *hub) roomCount() int {
h.mu.Lock()
defer h.mu.Unlock()
return len(h.rooms)
}
// playerIDFor is the seat a session holds. Declared here because the mapping // playerIDFor is the seat a session holds. Declared here because the mapping
// between a connection and a seat is registry knowledge, not game knowledge. // between a connection and a seat is registry knowledge, not game knowledge.
func playerIDFor(seatName string) game.PlayerID { return game.PlayerID(seatName) } func playerIDFor(seatName string) game.PlayerID { return game.PlayerID(seatName) }
+9
View File
@@ -0,0 +1,9 @@
package wsapi
// roomCount reports live rooms, so a test can assert that a room was evicted.
// Nothing in the server depends on it.
func (h *hub) roomCount() int {
h.mu.Lock()
defer h.mu.Unlock()
return len(h.rooms)
}
+1 -1
View File
@@ -28,7 +28,7 @@ type Config struct {
// that also serves the frontend. // that also serves the frontend.
AllowedOrigins []string AllowedOrigins []string
// WebDir is the built frontend. Empty, or missing on disk, serves the API // WebDir is the built frontend. Empty, or missing on disk, serves the API
// alone — which is the state until phase 6 produces a bundle. // alone, which is how the server runs before the frontend has been built.
WebDir string WebDir string
} }
+3 -1
View File
@@ -276,8 +276,10 @@ func (s *session) run() {
s.close() s.close()
wg.Wait() wg.Wait()
// readLoop only ever returns an error; a cancelled context is the one
// that means the server chose to close.
status, reason := websocket.StatusNormalClosure, "" status, reason := websocket.StatusNormalClosure, ""
if err != nil && !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
status, reason = websocket.StatusPolicyViolation, "protocol error" status, reason = websocket.StatusPolicyViolation, "protocol error"
} }
_ = s.conn.Close(status, reason) _ = s.conn.Close(status, reason)
+6 -4
View File
@@ -878,8 +878,8 @@ func TestSanitizeNickname(t *testing.T) {
{"empty falls back", "", defaultNickname}, {"empty falls back", "", defaultNickname},
{"whitespace only falls back", " \t\n ", defaultNickname}, {"whitespace only falls back", " \t\n ", defaultNickname},
{"control characters stripped", "Mi\x00nh\x07", "Minh"}, {"control characters stripped", "Mi\x00nh\x07", "Minh"},
{"zero width stripped", "Mi​nh‍", "Minh"}, {"zero width stripped", "Mi\u200bnh\u200d", "Minh"},
{"bidi override stripped", "Minh‮", "Minh"}, {"bidi override stripped", "Minh\u202e", "Minh"},
{"whitespace collapsed", " Minh Nguyen ", "Minh Nguyen"}, {"whitespace collapsed", " Minh Nguyen ", "Minh Nguyen"},
{"newlines become spaces", "Minh\nNguyen", "Minh Nguyen"}, {"newlines become spaces", "Minh\nNguyen", "Minh Nguyen"},
{"over length truncated", strings.Repeat("a", 40), strings.Repeat("a", maxNicknameRunes)}, {"over length truncated", strings.Repeat("a", 40), strings.Repeat("a", maxNicknameRunes)},
@@ -939,8 +939,10 @@ func TestBucketRefills(t *testing.T) {
now := time.Now() now := time.Now()
b := newBucket(5, 2, now) b := newBucket(5, 2, now)
if !b.allow(now) || !b.allow(now) { for i := range 2 {
t.Fatal("burst should cover the first two") if !b.allow(now) {
t.Fatalf("burst should cover the first two, call %d refused", i+1)
}
} }
if b.allow(now) { if b.allow(now) {
t.Fatal("third call should exhaust the bucket") t.Fatal("third call should exhaust the bucket")
+3 -3
View File
@@ -57,9 +57,9 @@ const DICTIONARY_WORDS = [
]; ];
/** /**
* A budget that binds. The bundle is around 190 KB, so this leaves room for the * A budget that binds. The bundle is around 190 KB, so this leaves room for
* phase-7 screens without leaving room for a wordlist: the derived dictionary * more screens without leaving room for a wordlist: the derived dictionary is
* is about 3 MB, and even a fraction of it would not fit here. * several MB, and even a fraction of it would not fit here.
*/ */
const MAX_TOTAL_BYTES = 400_000; const MAX_TOTAL_BYTES = 400_000;