Files
noitu/web/tests/error-codes.test.js
T
tiennm99 55abdb36c1 refactor(wsapi): dedupe routing and seating, fix ordering races
Route room inputs through one toRoom helper, iterate live seats with
connected(), and share seating and reconnect-window code. Declare every
error code once in errcodes.go, which the web vocabulary test now reads.

Fixes:
- a kicked player is released before being told, so their next action
  is answered not_in_a_room
- a rejected word is counted before move_rejected is sent
- room code draw and registration share one lock hold
- two concurrent resumes with one token can no longer both take a seat
- multiplayer tests ready guests one at a time instead of racing

Behaviour changes:
- opening or joining another room while a game is running is refused
  with already_in_a_game
- resigning in the lobby answers game_not_started
2026-09-28 15:19:59 +07:00

71 lines
2.7 KiB
JavaScript

// ServerError.code is a UI key, so the server decides the vocabulary and this
// file has to speak all of it. The proto schema cannot help here — the codes
// are strings in the Go transport, not an enum — so the guard reads them out
// of the one file that declares them, server/internal/wsapi/errcodes.go.
//
// Without this, adding a code on the server degrades silently to the generic
// fallback: the player is told "something went wrong" for a situation the
// server described precisely.
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { errorMessages } from '../src/lib/i18n/vi.js';
const wsapiDir = fileURLToPath(new URL('../../server/internal/wsapi', import.meta.url));
const registry = 'errcodes.go';
/** Every code declared in the registry, as its wire string. */
function serverErrorCodes() {
const source = readFileSync(join(wsapiDir, registry), 'utf8');
return new Set([...source.matchAll(/errCode = "([a-z_]+)"/g)].map(([, code]) => code));
}
/**
* Call sites that hand an emitter a string literal instead of a registry
* constant. Go converts an untyped literal to errCode silently, so this is
* what keeps a new code from bypassing the list the rest of this file checks.
*/
function literalCallSites() {
const files = readdirSync(wsapiDir).filter(
(f) => f.endsWith('.go') && !f.endsWith('_test.go') && f !== registry
);
return files.flatMap((file) =>
[
...readFileSync(join(wsapiDir, file), 'utf8').matchAll(
/\b(?:errorMsg|broadcastError|toRoom)\([^)]*"/g
)
].map(([call]) => `${file}: ${call}`)
);
}
describe('server error codes', () => {
const codes = serverErrorCodes();
it('finds the codes at all, so an empty match cannot pass as agreement', () => {
// A renamed type or moved registry would otherwise turn this whole file
// green by finding nothing to check.
expect(codes.size).toBeGreaterThan(10);
expect(codes.has('room_not_found')).toBe(true);
});
it('declares every code in the registry, never inline at a call site', () => {
expect(literalCallSites()).toEqual([]);
});
it('has a Vietnamese message for every code the server can send', () => {
const missing = [...codes].filter((code) => !(code in errorMessages)).sort();
expect(missing).toEqual([]);
});
it('carries no message for a code the server cannot send', () => {
// Dead copy is a smaller problem than a missing message, but it is still
// a claim about the server that has stopped being true.
const stale = Object.keys(errorMessages)
.filter((code) => !codes.has(code))
.sort();
expect(stale).toEqual([]);
});
});