diff --git a/.github/workflows/proto.yml b/.github/workflows/proto.yml index d95be8d..c3939ca 100644 --- a/.github/workflows/proto.yml +++ b/.github/workflows/proto.yml @@ -43,23 +43,10 @@ jobs: - name: Lint the schema run: buf lint - # Skipped only until this contract first lands on main; after that the - # baseline always exists and the check is unconditional. - - name: Look for a breaking-change baseline on main - id: baseline - run: | - if git cat-file -e origin/main:proto/noitu/v1/game.proto 2>/dev/null; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - echo "::notice::no schema on main yet, skipping the breaking-change check" - fi - # ref=origin/main, not branch=main: a pull_request checkout is a detached # HEAD with no local main, and branch=main fails there with a git clone # error rather than a breaking-change report. - name: Reject breaking changes - if: steps.baseline.outputs.exists == 'true' run: buf breaking --against '.git#ref=origin/main' # A committed generated tree that no longer matches the schema is worse diff --git a/.gitignore b/.gitignore index d9ec25d..d8ac856 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,6 @@ web/.svelte-kit/ node_modules/ # Playwright -/test-results/ -/playwright-report/ web/test-results/ web/playwright-report/ diff --git a/Makefile b/Makefile index e5f69e4..0f7806c 100644 --- a/Makefile +++ b/Makefile @@ -62,8 +62,8 @@ fixture-dict: $(FIXTURE_DB) proto: web/node_modules buf generate -# What CI runs: the schema is well-formed, and the committed generated trees -# match what the schema currently produces. +# The local equivalent of proto.yml: the schema is well-formed, and the +# committed generated trees match what the schema currently produces. # --intent-to-add makes a newly emitted file visible: git diff alone ignores # untracked files and would call an incomplete committed tree clean. proto-check: proto diff --git a/NOTICE b/NOTICE index 33a7280..24a05a3 100644 --- a/NOTICE +++ b/NOTICE @@ -10,7 +10,7 @@ This product is distributed under two licenses, applying to different artifacts. All source code in this repository is licensed under the Apache License, Version 2.0. See the LICENSE file at the repository root. -This covers everything under server/, tools/, web/, and proto/. +This covers everything under server/, web/, and proto/. ------------------------------------------------------------------------------ 2. DICTIONARY DATA — Creative Commons Attribution-ShareAlike 4.0 International diff --git a/README.md b/README.md index d88c723..ed5af0d 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,8 @@ there is one implementation of it and not two. ## Status -Playable: vs bot at three difficulties, and online rooms of two to four by room code. See -[`plans/260904-1125-noi-tu-web-game/plan.md`](./plans/260904-1125-noi-tu-web-game/plan.md) -for the implementation plan and phase breakdown. +Playable: vs bot at three difficulties, and online rooms of two to four by room code. +Design history lives under `plans/`. ## Architecture @@ -169,9 +168,7 @@ The export is fetched fresh, not pinned: kaikki.org re-exports Wiktionary about keeps no dated snapshots, so two builds a week apart can differ slightly. The database records the SHA-256 of the file it was built from in its `meta` table. Neither the export nor the derived database is committed; both are build artifacts. See -[`data/ATTRIBUTION.md`](./data/ATTRIBUTION.md). Leftover `data/dictionary.db` or -`data/undertheseanlp-words.jsonl` files from earlier sources are no longer read and can be -deleted. +[`data/ATTRIBUTION.md`](./data/ATTRIBUTION.md). ## Running the server @@ -231,9 +228,11 @@ dev-only URL to get wrong. | `proto` | Regenerate the Go and JS wire types from `proto/` (needs `buf`) | | `proto-check` | Lint the schema and verify the committed generated code is in sync | | `fixture-dict` | Build the small test dictionary, no download needed | -| `test` | Run Go and JavaScript tests | +| `test` | Run Go and JavaScript tests (`test-go`, `test-web`) | | `test-e2e` | Run the Playwright suite against the fixture dictionary | | `run` | Build and run the server locally | +| `clean` | Remove build artifacts, keeping the downloaded export | +| `help` | List these targets | ### Without `make` diff --git a/proto/testdata/client_hello.bin b/proto/testdata/client_hello.bin index 2d84153..dcd5645 100644 --- a/proto/testdata/client_hello.bin +++ b/proto/testdata/client_hello.bin @@ -1,2 +1,2 @@ -$r-8f2cNgười chơi ẩn danh \ No newline at end of file +$r-8f2cNgười chơi ẩn danh \ No newline at end of file diff --git a/proto/testdata/server_game_started.bin b/proto/testdata/server_game_started.bin index 411352a..d9fc40d 100644 --- a/proto/testdata/server_game_started.bin +++ b/proto/testdata/server_game_started.bin @@ -1,6 +1,6 @@ "n -hòa bìnhbình  ×ø©‘3(0 œ: +hòa bìnhbình  ×ø©‘3(0°ê: p1Người chơi 0: p2 Khách mời *(0: p3Khách 2 Bp1 \ No newline at end of file diff --git a/server/cmd/build-dictionary/filter.go b/server/cmd/build-dictionary/filter.go index 849ce29..bd32da5 100644 --- a/server/cmd/build-dictionary/filter.go +++ b/server/cmd/build-dictionary/filter.go @@ -17,7 +17,6 @@ type rejectReason string const ( rejectEmpty rejectReason = "empty" rejectTooShort rejectReason = "fewer than 2 syllables" - rejectTooLong rejectReason = "over max-syllables" rejectDigit rejectReason = "contains a digit" rejectPunct rejectReason = "contains punctuation" rejectNonVietnam rejectReason = "no Vietnamese letters" @@ -27,8 +26,8 @@ const ( ) // accept normalizes a raw source entry and decides whether it belongs in the -// game dictionary. maxSyllables of 0 means no upper bound. -func accept(raw string, maxSyllables int) (word string, syllables []string, reason rejectReason, ok bool) { +// game dictionary. +func accept(raw string) (word string, syllables []string, reason rejectReason, ok bool) { word, syllables, err := vietnamese.Normalize(raw) if err != nil { return "", nil, rejectEmpty, false @@ -37,9 +36,6 @@ func accept(raw string, maxSyllables int) (word string, syllables []string, reas if !vietnamese.HasEnoughSyllables(syllables) { return "", nil, rejectTooShort, false } - if maxSyllables > 0 && len(syllables) > maxSyllables { - return "", nil, rejectTooLong, false - } for _, r := range word { switch { diff --git a/server/cmd/build-dictionary/filter_test.go b/server/cmd/build-dictionary/filter_test.go index 2024e6f..fea9009 100644 --- a/server/cmd/build-dictionary/filter_test.go +++ b/server/cmd/build-dictionary/filter_test.go @@ -17,7 +17,7 @@ func TestAcceptKeepsValidWords(t *testing.T) { } for _, tc := range tests { - word, syllables, reason, ok := accept(tc.raw, 0) + word, syllables, reason, ok := accept(tc.raw) if !ok { t.Errorf("accept(%q) rejected: %s", tc.raw, reason) continue @@ -55,7 +55,7 @@ func TestAcceptRejects(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, _, reason, ok := accept(tc.raw, 0) + _, _, reason, ok := accept(tc.raw) if ok { t.Fatalf("accept(%q) succeeded, want rejection %s", tc.raw, tc.want) } @@ -65,21 +65,3 @@ func TestAcceptRejects(t *testing.T) { }) } } - -func TestAcceptMaxSyllables(t *testing.T) { - const raw = "công nghiệp hóa" - - if _, _, _, ok := accept(raw, 0); !ok { - t.Errorf("accept(%q, no limit) rejected, want accepted", raw) - } - if _, _, _, ok := accept(raw, 3); !ok { - t.Errorf("accept(%q, max 3) rejected, want accepted", raw) - } - _, _, reason, ok := accept(raw, 2) - if ok { - t.Fatalf("accept(%q, max 2) accepted, want rejection", raw) - } - if reason != rejectTooLong { - t.Errorf("reason = %s, want %s", reason, rejectTooLong) - } -} diff --git a/server/cmd/build-dictionary/kaikki_list.go b/server/cmd/build-dictionary/kaikki_list.go index 4203f48..1f52b14 100644 --- a/server/cmd/build-dictionary/kaikki_list.go +++ b/server/cmd/build-dictionary/kaikki_list.go @@ -49,7 +49,7 @@ type kaikkiProvenance struct { // Lines are read with bufio.Reader rather than bufio.Scanner because a row // carries every sense and translation of its entry and can run to hundreds of // kilobytes; a scanner's fixed cap would be a guess that eventually fails. -func readKaikkiList(path string, maxSyllables int) (map[string]entry, map[rejectReason]int, map[string]int, kaikkiProvenance, error) { +func readKaikkiList(path string) (map[string]entry, map[rejectReason]int, map[string]int, kaikkiProvenance, error) { var prov kaikkiProvenance f, err := os.Open(path) @@ -92,7 +92,7 @@ func readKaikkiList(path string, maxSyllables int) (map[string]entry, map[reject rejects[rejectNotVietnamese]++ } else { pos[row.Pos]++ - if word, syllables, reason, ok := accept(row.Word, maxSyllables); !ok { + if word, syllables, reason, ok := accept(row.Word); !ok { rejects[reason]++ } else { words[word] = entry{ @@ -153,13 +153,12 @@ func formatPosTally(pos map[string]int) string { // kaikkiSourceSpec describes a kaikki build for the meta table. With no commit // or checksum pinned upstream, the hash and row count of the bytes read are the // provenance. -func kaikkiSourceSpec(path string, prov kaikkiProvenance, maxSyllables int) sourceSpec { +func kaikkiSourceSpec(path string, prov kaikkiProvenance) sourceSpec { return sourceSpec{ - table: "kaikki:" + filepath.Base(path), - url: kaikkiSourceURL, - license: "CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)", - attribution: "See data/ATTRIBUTION.md for required attribution and the list of modifications.", - maxSyllables: maxSyllables, + table: "kaikki:" + filepath.Base(path), + url: kaikkiSourceURL, + license: "CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)", + attribution: "See data/ATTRIBUTION.md for required attribution and the list of modifications.", extra: [][2]string{ {"source_sha256", prov.sha256}, {"source_rows", fmt.Sprint(prov.rows)}, diff --git a/server/cmd/build-dictionary/kaikki_list_test.go b/server/cmd/build-dictionary/kaikki_list_test.go index f408711..54e3bd4 100644 --- a/server/cmd/build-dictionary/kaikki_list_test.go +++ b/server/cmd/build-dictionary/kaikki_list_test.go @@ -32,7 +32,7 @@ func defaultKaikkiLines() []string { } func TestKaikkiListKeepsVietnameseEntries(t *testing.T) { - words, rejects, pos, prov, err := readKaikkiList(fixtureKaikki(t, defaultKaikkiLines()...), 0) + words, rejects, pos, prov, err := readKaikkiList(fixtureKaikki(t, defaultKaikkiLines()...)) if err != nil { t.Fatal(err) } @@ -59,7 +59,7 @@ func TestKaikkiListKeepsVietnameseEntries(t *testing.T) { func TestKaikkiListHashesTheBytesItRead(t *testing.T) { path := fixtureKaikki(t, defaultKaikkiLines()...) - _, _, _, prov, err := readKaikkiList(path, 0) + _, _, _, prov, err := readKaikkiList(path) if err != nil { t.Fatal(err) } @@ -104,7 +104,7 @@ func TestKaikkiListHandlesDownloadShapes(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - words, _, _, prov, err := readKaikkiList(fixtureKaikkiRaw(t, tc.raw), 0) + words, _, _, prov, err := readKaikkiList(fixtureKaikkiRaw(t, tc.raw)) if tc.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("err = %v, want one containing %q", err, tc.wantErr) @@ -126,7 +126,7 @@ func TestKaikkiListNamesMalformedLine(t *testing.T) { `{"word": "học sinh", "pos": "noun", "lang_code": "vi"}`, `{"word": "broken"`, ) - _, _, _, _, err := readKaikkiList(path, 0) + _, _, _, _, err := readKaikkiList(path) if err == nil { t.Fatal("malformed line was skipped, want error") } @@ -140,7 +140,7 @@ func TestKaikkiListReadsLongLines(t *testing.T) { // scanner buffer; the reader must not have a line cap. padding := strings.Repeat("x", 2<<20) path := fixtureKaikki(t, `{"word": "học sinh", "pos": "noun", "lang_code": "vi", "note": "`+padding+`"}`) - words, _, _, _, err := readKaikkiList(path, 0) + words, _, _, _, err := readKaikkiList(path) if err != nil { t.Fatal(err) } diff --git a/server/cmd/build-dictionary/main.go b/server/cmd/build-dictionary/main.go index f136d26..65818bb 100644 --- a/server/cmd/build-dictionary/main.go +++ b/server/cmd/build-dictionary/main.go @@ -33,17 +33,16 @@ import ( // builderVer changes whenever the meta table's contract does, so two databases // with different provenance rows never claim the same builder. -const builderVer = "3" +const builderVer = "4" type config struct { // kaikki is the corpus: the upstream wiktextract JSONL export. kaikki string // words is an alternative source: a plain list, one word per line, used to // build a small fixture database without the upstream download. - words string - out string - maxSyllables int - minWords int + words string + out string + minWords int } func main() { @@ -53,7 +52,6 @@ func main() { flag.StringVar(&cfg.kaikki, "kaikki", "", "upstream kaikki.org wiktextract JSONL export to read") flag.StringVar(&cfg.words, "words", "", "read a plain word list instead of the upstream export (one word per line, # comments)") flag.StringVar(&cfg.out, "out", "../data/noitu.db", "derived database to write") - flag.IntVar(&cfg.maxSyllables, "max-syllables", 0, "reject words longer than this (0 = no limit)") flag.IntVar(&cfg.minWords, "min-words", 30000, "fail if fewer words survive filtering") flag.Parse() @@ -84,7 +82,7 @@ func runFromKaikkiList(cfg config) error { return fmt.Errorf("kaikki export not found at %s — run 'make fetch-dict' first: %w", cfg.kaikki, err) } - words, rejects, pos, prov, err := readKaikkiList(cfg.kaikki, cfg.maxSyllables) + words, rejects, pos, prov, err := readKaikkiList(cfg.kaikki) if err != nil { return err } @@ -92,7 +90,7 @@ func runFromKaikkiList(cfg config) error { log.Printf("parts of speech: %s", formatPosTally(pos)) log.Printf("accepted %d distinct words from %s (%d rows, sha256 %s)", len(words), cfg.kaikki, prov.rows, prov.sha256) - return finish(cfg, words, kaikkiSourceSpec(cfg.kaikki, prov, cfg.maxSyllables)) + return finish(cfg, words, kaikkiSourceSpec(cfg.kaikki, prov)) } // finish is the tail every input mode shares: the size floor, alias @@ -120,7 +118,7 @@ func finish(cfg config, words map[string]entry, src sourceSpec) error { } // runFromWordList derives a database from a plain list of words instead of the -// upstream release. +// upstream export. // // It exists so tests and CI have a real dictionary to play against without the // upstream download. The filtering, alias generation, writing and verification @@ -141,7 +139,7 @@ func runFromWordList(cfg config) error { if line == "" || strings.HasPrefix(line, "#") { continue } - word, syllables, reason, ok := accept(line, cfg.maxSyllables) + word, syllables, reason, ok := accept(line) if !ok { rejects[reason]++ continue @@ -222,8 +220,7 @@ type entry struct { type sourceSpec struct { // table names the input: "kaikki:" for the corpus, "wordlist:" // for a fixture, so the output says which build produced it. - table string - maxSyllables int + table string // url is the upstream artifact; empty for fixture builds. url string // license and attribution describe the data's licence obligations. The @@ -407,7 +404,6 @@ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); {"word_count", fmt.Sprint(len(words))}, {"alias_count", fmt.Sprint(len(aliases))}, {"source_table", src.table}, - {"max_syllables", fmt.Sprint(src.maxSyllables)}, } meta = append(meta, src.extra...) for _, kv := range meta { diff --git a/server/cmd/build-dictionary/main_test.go b/server/cmd/build-dictionary/main_test.go index fabc78e..79ac971 100644 --- a/server/cmd/build-dictionary/main_test.go +++ b/server/cmd/build-dictionary/main_test.go @@ -39,15 +39,14 @@ func defaultRows() [][2]string { } } -func buildFixture(t *testing.T, rows [][2]string, maxSyllables int) string { +func buildFixture(t *testing.T, rows [][2]string) string { t.Helper() out := filepath.Join(t.TempDir(), "noitu.db") cfg := config{ - kaikki: fixtureSource(t, rows), - out: out, - maxSyllables: maxSyllables, - minWords: 1, + kaikki: fixtureSource(t, rows), + out: out, + minWords: 1, } if err := run(cfg); err != nil { t.Fatalf("run: %v", err) @@ -66,7 +65,7 @@ func openOut(t *testing.T, path string) *sql.DB { } func TestBuildProducesExpectedWords(t *testing.T) { - db := openOut(t, buildFixture(t, defaultRows(), 0)) + db := openOut(t, buildFixture(t, defaultRows())) var count int if err := db.QueryRow(`SELECT COUNT(*) FROM words`).Scan(&count); err != nil { @@ -99,7 +98,7 @@ func TestBuildProducesExpectedWords(t *testing.T) { } func TestBuildComputesOutDegree(t *testing.T) { - db := openOut(t, buildFixture(t, defaultRows(), 0)) + db := openOut(t, buildFixture(t, defaultRows())) // "pháp luật" and "pháp" (rejected) mean exactly one word starts with "pháp". assertOutDegree(t, db, "pháp", 1) @@ -121,7 +120,7 @@ func assertOutDegree(t *testing.T, db *sql.DB, syllable string, want int) { } func TestBuildWritesAliases(t *testing.T) { - db := openOut(t, buildFixture(t, defaultRows(), 0)) + db := openOut(t, buildFixture(t, defaultRows())) var canonical string err := db.QueryRow(`SELECT canonical FROM aliases WHERE variant = ?`, "hoà bình").Scan(&canonical) @@ -146,7 +145,7 @@ func TestBuildWritesAliases(t *testing.T) { } func TestBuildRecordsProvenance(t *testing.T) { - db := openOut(t, buildFixture(t, defaultRows(), 0)) + db := openOut(t, buildFixture(t, defaultRows())) for _, key := range []string{"source_url", "source_license", "attribution", "built_at", "word_count"} { var value string @@ -176,7 +175,7 @@ func TestBuildFailsBelowMinWords(t *testing.T) { // A failed build must leave the previous good database untouched. Building in // place would delete it and leave an empty file the server would happily open. func TestFailedBuildPreservesPreviousOutput(t *testing.T) { - out := buildFixture(t, defaultRows(), 0) + out := buildFixture(t, defaultRows()) before, err := os.ReadFile(out) if err != nil { diff --git a/server/internal/wsapi/wire_test.go b/server/internal/wsapi/wire_test.go index e3015d9..6686b78 100644 --- a/server/internal/wsapi/wire_test.go +++ b/server/internal/wsapi/wire_test.go @@ -28,7 +28,7 @@ const fixtureDir = "../../../proto/testdata" func clientVariants() map[string]*noituv1.ClientMessage { return map[string]*noituv1.ClientMessage{ "client_hello": {Payload: &noituv1.ClientMessage_Hello{Hello: &noituv1.Hello{ - ProtocolVersion: 1, + ProtocolVersion: ProtocolVersion, ResumeToken: "r-8f2c", Nickname: "Người chơi ẩn danh", }}}, @@ -50,11 +50,11 @@ func clientVariants() map[string]*noituv1.ClientMessage { "client_set_ready": {Payload: &noituv1.ClientMessage_SetReady{SetReady: &noituv1.SetReady{ Ready: true, }}}, - "client_start_game": {Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}}, + "client_start_game": {Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}}, "client_kick_player": {Payload: &noituv1.ClientMessage_KickPlayer{KickPlayer: &noituv1.KickPlayer{ PlayerId: "p3", }}}, - "client_leave_room": {Payload: &noituv1.ClientMessage_LeaveRoom{LeaveRoom: &noituv1.LeaveRoom{}}}, + "client_leave_room": {Payload: &noituv1.ClientMessage_LeaveRoom{LeaveRoom: &noituv1.LeaveRoom{}}}, "client_send_chat": {Payload: &noituv1.ClientMessage_SendChat{SendChat: &noituv1.SendChat{ Text: "Chào bạn, ván này khó thật!", }}}, @@ -76,7 +76,7 @@ func serverVariants() map[string]*noituv1.ServerMessage { MyTurn: true, DeadlineUnixMs: 1756998020000, TurnSeq: 1, - TurnLimitMs: 20000, + TurnLimitMs: 30000, Players: fixtureScores(), TurnPlayerId: "p1", }}}, diff --git a/testdata/fixture-words.txt b/testdata/fixture-words.txt index 5720926..1a849a7 100644 --- a/testdata/fixture-words.txt +++ b/testdata/fixture-words.txt @@ -2,7 +2,7 @@ # # Hand-written common Vietnamese compounds, not an extract of the upstream # dictionary: CI must never download the upstream wordlist, and a fixture derived -# from it would carry that release's licence into every test artifact. +# from it would carry that export's licence into every test artifact. # # `make fixture-dict` turns this into a real derived database through the same # filter, alias and write path production uses, so a fixture cannot drift into diff --git a/web/e2e/fixture-dictionary.js b/web/e2e/fixture-dictionary.js index c2c433b..06c279d 100644 --- a/web/e2e/fixture-dictionary.js +++ b/web/e2e/fixture-dictionary.js @@ -56,7 +56,7 @@ export const ONE_SYLLABLE_WORD = 'sinh'; * an unhelpful "game_start_failed". Asserting it here turns that into a * sentence a reader can act on. */ -export const HUB_SYLLABLE = 'sinh'; +const HUB_SYLLABLE = 'sinh'; const MIN_OPENING_OUT_DEGREE = 20; const hubContinuations = (byFirstSyllable.get(HUB_SYLLABLE) ?? []).length; @@ -66,5 +66,3 @@ if (hubContinuations < MIN_OPENING_OUT_DEGREE) { `the server needs ${MIN_OPENING_OUT_DEGREE} before it will open a game on that syllable` ); } - -export { words }; diff --git a/web/package.json b/web/package.json index fd7554d..bc6db30 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,6 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "vite preview", "prepare": "svelte-kit sync", "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", "test": "vite build && vitest run", diff --git a/web/src/app.css b/web/src/app.css index d1c3c5b..682ec4c 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -30,7 +30,6 @@ --radius: 12px; --radius-sm: 8px; - --gap: 12px; --font: system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; color-scheme: light; diff --git a/web/src/lib/i18n/vi.js b/web/src/lib/i18n/vi.js index c78a1b8..c30b670 100644 --- a/web/src/lib/i18n/vi.js +++ b/web/src/lib/i18n/vi.js @@ -34,12 +34,10 @@ export const t = { you: 'Bạn', opponent: 'Đối thủ', someone: 'Người chơi', - scoreLabel: 'Điểm', syllableUnit: 'tiếng', correctedFrom: 'Bạn gõ “{typed}”, từ đúng là “{word}”.', secondsLeft: '{n} giây', - waiting: 'Đang chờ…', connecting: 'Đang kết nối…', connected: 'Đã kết nối', reconnecting: 'Mất kết nối, đang thử lại…', @@ -77,7 +75,6 @@ export const t = { copyLink: 'Sao chép liên kết mời', shareLink: 'Chia sẻ', copied: 'Đã sao chép', - waitingForOpponent: 'Đang chờ người chơi vào phòng…', leaveRoom: 'Rời phòng', shareInvite: 'Vào chơi nối từ với tôi', diff --git a/web/src/lib/stores/game.svelte.js b/web/src/lib/stores/game.svelte.js index 03eade6..5cb1ece 100644 --- a/web/src/lib/stores/game.svelte.js +++ b/web/src/lib/stores/game.svelte.js @@ -492,9 +492,6 @@ export function createGameStore() { ); }, - clearRejection() { - state.rejection = null; - }, clearError() { state.error = null; }, diff --git a/web/src/lib/ws/client.js b/web/src/lib/ws/client.js index 19055b3..390d499 100644 --- a/web/src/lib/ws/client.js +++ b/web/src/lib/ws/client.js @@ -39,7 +39,7 @@ export const LIVENESS_TIMEOUT_MS = 3 * PING_INTERVAL_MS; * client reconnects every half second forever after a deploy that bumps the * protocol. */ -export const TERMINAL_ERROR_CODES = new Set(['protocol_version_mismatch']); +const TERMINAL_ERROR_CODES = new Set(['protocol_version_mismatch']); const RESUME_KEY = 'noitu.resumeToken'; diff --git a/web/tests/dictionary-source.test.js b/web/tests/dictionary-source.test.js index 0a1d756..a1eb9be 100644 --- a/web/tests/dictionary-source.test.js +++ b/web/tests/dictionary-source.test.js @@ -1,10 +1,12 @@ -// The upstream dictionary URL lives in two places: the Makefile, which builds -// it for a developer, and the Dockerfile, which builds it for the image. They -// have to agree, or the container ships a wordlist nobody tested against. +// The upstream dictionary URL lives in three places: the Makefile, which +// builds it for a developer, the Dockerfile, which builds it for the image, and +// the builder, which stamps it into the database. They have to agree, or the +// container ships a wordlist nobody tested against. The docs that quote the +// URL are held to the same copy. // // This lives in the JavaScript suite for no better reason than that it is the -// suite that already reads other files in the repository. It is checking two -// build files, not the frontend. +// suite that already reads other files in the repository. It is checking build +// files and docs, not the frontend. import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -49,4 +51,11 @@ describe('the upstream dictionary export', () => { const builderUrl = pin(builder, /kaikkiSourceURL\s*=\s*"([^"]+)"/, 'kaikkiSourceURL in the builder'); expect(builderUrl).toBe(makeUrl); }); + + it('is the URL the docs quote', () => { + for (const rel of ['../../README.md', '../../data/ATTRIBUTION.md']) { + const doc = readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8'); + expect(doc, `${rel} does not quote DICT_URL`).toContain(makeUrl); + } + }); });