cleanup testing utilities, documentation, and AI commentary

This commit is contained in:
seavor 2026-04-18 15:32:50 -05:00
parent bd2382c94e
commit ef6cea6f6c
150 changed files with 891 additions and 1233 deletions

View file

@ -29,13 +29,14 @@ vi.mock('./services/ProtobufService', () => ({
import { WebClient } from './WebClient';
import { WebSocketService } from './services/WebSocketService';
import { ProtobufService } from './services/ProtobufService';
import { StatusEnum } from './interfaces/StatusEnum';
import { StatusEnum } from './types/StatusEnum';
import { Subject } from 'rxjs';
import { Mock } from 'vitest';
import { SocketTransport } from './services/ProtobufService';
import { WebSocketServiceConfig } from './services/WebSocketService';
import type { IWebClientResponse, IWebClientRequest } from './interfaces';
import type { ConnectTarget } from './interfaces/WebClientConfig';
import type { IWebClientResponse } from './types/WebClientResponse';
import type { IWebClientRequest } from './types/WebClientRequest';
import type { ConnectTarget } from './types/WebClientConfig';
import { installMockWebSocket } from './__mocks__/helpers';
function makeMockResponse(): IWebClientResponse {

View file

@ -1,11 +1,9 @@
import { ping } from './commands/session';
import { CLIENT_OPTIONS } from './config';
import type {
ConnectTarget,
IWebClientRequest,
IWebClientResponse,
} from './interfaces';
import { StatusEnum } from './interfaces';
import type { ConnectTarget } from './types/WebClientConfig';
import type { IWebClientRequest } from './types/WebClientRequest';
import type { IWebClientResponse } from './types/WebClientResponse';
import { StatusEnum } from './types/StatusEnum';
import { ProtobufService } from './services/ProtobufService';
import { WebSocketService } from './services/WebSocketService';

View file

@ -17,9 +17,6 @@
* property, not a getter that throws.
*/
// ---------------------------------------------------------------------------
// response.session (ISessionResponse)
// ---------------------------------------------------------------------------
const session = {
initialized: vi.fn(),
connectionAttempted: vi.fn(),
@ -80,9 +77,6 @@ const session = {
replayDownloaded: vi.fn(),
};
// ---------------------------------------------------------------------------
// response.room (IRoomResponse)
// ---------------------------------------------------------------------------
const room = {
clearStore: vi.fn(),
joinRoom: vi.fn(),
@ -97,9 +91,6 @@ const room = {
joinedGame: vi.fn(),
};
// ---------------------------------------------------------------------------
// response.game (IGameResponse)
// ---------------------------------------------------------------------------
const game = {
clearStore: vi.fn(),
gameStateChanged: vi.fn(),
@ -133,9 +124,6 @@ const game = {
zonePropertiesChanged: vi.fn(),
};
// ---------------------------------------------------------------------------
// response.admin (IAdminResponse)
// ---------------------------------------------------------------------------
const admin = {
adjustMod: vi.fn(),
reloadConfig: vi.fn(),
@ -143,9 +131,6 @@ const admin = {
updateServerMessage: vi.fn(),
};
// ---------------------------------------------------------------------------
// response.moderator (IModeratorResponse)
// ---------------------------------------------------------------------------
const moderator = {
banFromServer: vi.fn(),
banHistory: vi.fn(),
@ -159,9 +144,6 @@ const moderator = {
updateAdminNotes: vi.fn(),
};
// ---------------------------------------------------------------------------
// Exported mock — replaces the real WebClient module for all consumers.
// ---------------------------------------------------------------------------
export const WebClient = {
_instance: null as any,
instance: {

View file

@ -1,27 +1,6 @@
/**
* Shared mock factories for websocket layer unit tests.
* Import the helpers you need in each spec file via:
* import { makeMockWebSocket, useWebClientCleanup } from '../__mocks__/helpers';
*/
import { WebClient } from '../WebClient';
/**
* Resets the WebClient singleton to null. Call directly, or use
* `useWebClientCleanup()` to register automatic beforeEach/afterEach hooks.
*/
export function resetWebClientSingleton() {
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
}
/**
* Registers beforeEach/afterEach hooks that reset the WebClient singleton.
* Call at describe-level or file-level in any spec that mocks WebClient.
* Prevents isolate:false singleton leakage between spec files.
*/
export function useWebClientCleanup() {
beforeEach(() => resetWebClientSingleton());
afterEach(() => resetWebClientSingleton());
}
/** Builds a mock WebSocket instance */
export function makeMockWebSocketInstance() {

View file

@ -20,9 +20,6 @@ const { invokeOnSuccess } = makeCallbackHelpers(
2
);
// ----------------------------------------------------------------
// adjustMod
// ----------------------------------------------------------------
describe('adjustMod', () => {
it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => {
@ -41,9 +38,6 @@ describe('adjustMod', () => {
});
});
// ----------------------------------------------------------------
// reloadConfig
// ----------------------------------------------------------------
describe('reloadConfig', () => {
it('calls sendAdminCommand with Command_ReloadConfig extension', () => {
@ -62,9 +56,6 @@ describe('reloadConfig', () => {
});
});
// ----------------------------------------------------------------
// shutdownServer
// ----------------------------------------------------------------
describe('shutdownServer', () => {
it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => {
@ -83,9 +74,6 @@ describe('shutdownServer', () => {
});
});
// ----------------------------------------------------------------
// updateServerMessage
// ----------------------------------------------------------------
describe('updateServerMessage', () => {
it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => {

View file

@ -39,9 +39,6 @@ const { invokeOnSuccess } = makeCallbackHelpers(
2
);
// ----------------------------------------------------------------
// banFromServer
// ----------------------------------------------------------------
describe('banFromServer', () => {
it('calls sendModeratorCommand with Command_BanFromServer', () => {
@ -60,9 +57,6 @@ describe('banFromServer', () => {
});
});
// ----------------------------------------------------------------
// forceActivateUser
// ----------------------------------------------------------------
describe('forceActivateUser', () => {
it('calls sendModeratorCommand with Command_ForceActivateUser', () => {
@ -79,9 +73,6 @@ describe('forceActivateUser', () => {
});
});
// ----------------------------------------------------------------
// getAdminNotes
// ----------------------------------------------------------------
describe('getAdminNotes', () => {
it('calls sendModeratorCommand with Command_GetAdminNotes', () => {
@ -101,9 +92,6 @@ describe('getAdminNotes', () => {
});
});
// ----------------------------------------------------------------
// getBanHistory
// ----------------------------------------------------------------
describe('getBanHistory', () => {
it('calls sendModeratorCommand with Command_GetBanHistory', () => {
@ -123,9 +111,6 @@ describe('getBanHistory', () => {
});
});
// ----------------------------------------------------------------
// getWarnHistory
// ----------------------------------------------------------------
describe('getWarnHistory', () => {
it('calls sendModeratorCommand with Command_GetWarnHistory', () => {
@ -145,9 +130,6 @@ describe('getWarnHistory', () => {
});
});
// ----------------------------------------------------------------
// getWarnList
// ----------------------------------------------------------------
describe('getWarnList', () => {
it('calls sendModeratorCommand with Command_GetWarnList', () => {
@ -167,9 +149,6 @@ describe('getWarnList', () => {
});
});
// ----------------------------------------------------------------
// grantReplayAccess
// ----------------------------------------------------------------
describe('grantReplayAccess', () => {
it('calls sendModeratorCommand with Command_GrantReplayAccess', () => {
@ -186,9 +165,6 @@ describe('grantReplayAccess', () => {
});
});
// ----------------------------------------------------------------
// updateAdminNotes
// ----------------------------------------------------------------
describe('updateAdminNotes', () => {
it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => {
@ -205,9 +181,6 @@ describe('updateAdminNotes', () => {
});
});
// ----------------------------------------------------------------
// viewLogHistory
// ----------------------------------------------------------------
describe('viewLogHistory', () => {
it('calls sendModeratorCommand with Command_ViewLogHistory', () => {
@ -229,9 +202,6 @@ describe('viewLogHistory', () => {
});
});
// ----------------------------------------------------------------
// warnUser
// ----------------------------------------------------------------
describe('warnUser', () => {
it('calls sendModeratorCommand with Command_WarnUser', () => {

View file

@ -24,9 +24,6 @@ const { invokeOnSuccess } = makeCallbackHelpers(
3
);
// ----------------------------------------------------------------
// createGame
// ----------------------------------------------------------------
describe('createGame', () => {
it('calls sendRoomCommand with Command_CreateGame', () => {
@ -43,9 +40,6 @@ describe('createGame', () => {
});
});
// ----------------------------------------------------------------
// joinGame
// ----------------------------------------------------------------
describe('joinGame', () => {
it('calls sendRoomCommand with Command_JoinGame', () => {
@ -62,9 +56,6 @@ describe('joinGame', () => {
});
});
// ----------------------------------------------------------------
// leaveRoom
// ----------------------------------------------------------------
describe('leaveRoom', () => {
it('calls sendRoomCommand with Command_LeaveRoom', () => {
@ -81,9 +72,6 @@ describe('leaveRoom', () => {
});
});
// ----------------------------------------------------------------
// roomSay
// ----------------------------------------------------------------
describe('roomSay', () => {
it('calls sendRoomCommand with trimmed message', () => {

View file

@ -6,10 +6,10 @@ import {
type ActivateParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { disconnect, login, updateStatus } from './';
export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void {

View file

@ -1,5 +1,5 @@
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
export function connect(target: ConnectTarget): void {
WebClient.instance.connect(target);

View file

@ -5,10 +5,10 @@ import {
type ForgotPasswordChallengeParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { disconnect, updateStatus } from './';
export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void {

View file

@ -6,10 +6,10 @@ import {
type ForgotPasswordRequestParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { disconnect, updateStatus } from './';
export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void {

View file

@ -6,10 +6,10 @@ import {
type ForgotPasswordResetParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { hashPassword } from '../../utils';
import { disconnect, updateStatus } from '.';

View file

@ -8,10 +8,10 @@ import {
type LoginParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { hashPassword } from '../../utils';
import {
disconnect,

View file

@ -8,10 +8,10 @@ import {
type RegisterParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { hashPassword } from '../../utils';
import { login, disconnect, updateStatus } from './';

View file

@ -7,10 +7,10 @@ import {
type RequestPasswordSaltParams,
} from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { CLIENT_CONFIG } from '../../config';
import { WebClient } from '../../WebClient';
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
import type { ConnectTarget } from '../../types/WebClientConfig';
import { updateStatus } from './';
export function requestPasswordSalt(

View file

@ -18,8 +18,16 @@ import { Mock } from 'vitest';
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
import { WebClient } from '../../WebClient';
import * as SessionIndexMocks from './';
import { Enriched } from '@app/types';
import { StatusEnum } from '../../interfaces/StatusEnum';
import {
WebSocketConnectReason,
type LoginConnectOptions,
type RegisterConnectOptions,
type ActivateConnectOptions,
type PasswordResetRequestConnectOptions,
type PasswordResetChallengeConnectOptions,
type PasswordResetConnectOptions,
} from '../../types/ConnectOptions';
import { StatusEnum } from '../../types/StatusEnum';
import {
Command_Activate_ext,
Command_ForgotPasswordChallenge_ext,
@ -56,50 +64,50 @@ const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpe
);
const baseTransport = { host: 'h', port: '1' };
const makeLoginOpts = (overrides: Partial<Enriched.LoginConnectOptions> = {}): Enriched.LoginConnectOptions => ({
const makeLoginOpts = (overrides: Partial<LoginConnectOptions> = {}): LoginConnectOptions => ({
...baseTransport,
userName: 'alice',
reason: Enriched.WebSocketConnectReason.LOGIN,
reason: WebSocketConnectReason.LOGIN,
...overrides,
});
const makeRegisterOpts = (
overrides: Partial<Enriched.RegisterConnectOptions> = {}
): Enriched.RegisterConnectOptions => ({
overrides: Partial<RegisterConnectOptions> = {}
): RegisterConnectOptions => ({
...baseTransport,
userName: 'alice',
password: 'pw',
email: 'a@b.com',
country: 'US',
realName: 'Al',
reason: Enriched.WebSocketConnectReason.REGISTER,
reason: WebSocketConnectReason.REGISTER,
...overrides,
});
const makeActivateOpts = (
overrides: Partial<Enriched.ActivateConnectOptions> = {}
): Enriched.ActivateConnectOptions => ({
overrides: Partial<ActivateConnectOptions> = {}
): ActivateConnectOptions => ({
...baseTransport,
userName: 'alice',
token: 'tok',
reason: Enriched.WebSocketConnectReason.ACTIVATE_ACCOUNT,
reason: WebSocketConnectReason.ACTIVATE_ACCOUNT,
...overrides,
});
const makeForgotRequestOpts = (): Enriched.PasswordResetRequestConnectOptions => ({
const makeForgotRequestOpts = (): PasswordResetRequestConnectOptions => ({
...baseTransport,
userName: 'alice',
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET_REQUEST,
reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST,
});
const makeForgotChallengeOpts = (): Enriched.PasswordResetChallengeConnectOptions => ({
const makeForgotChallengeOpts = (): PasswordResetChallengeConnectOptions => ({
...baseTransport,
userName: 'alice',
email: 'a@b.com',
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE,
reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE,
});
const makeForgotResetOpts = (): Enriched.PasswordResetConnectOptions => ({
const makeForgotResetOpts = (): PasswordResetConnectOptions => ({
...baseTransport,
userName: 'alice',
token: 'tok',
newPassword: 'newpw',
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET,
reason: WebSocketConnectReason.PASSWORD_RESET,
});
@ -109,9 +117,6 @@ beforeEach(() => {
(passwordSaltSupported as Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
// connect.ts
// ----------------------------------------------------------------
describe('connect', () => {
it('calls WebClient.instance.connect with the target', () => {
@ -128,9 +133,6 @@ describe('testConnect', () => {
});
});
// ----------------------------------------------------------------
// updateStatus.ts
// ----------------------------------------------------------------
describe('updateStatus', () => {
it('calls WebClient.instance.response.session.updateStatus and WebClient.instance.updateStatus', () => {
@ -140,9 +142,6 @@ describe('updateStatus', () => {
});
});
// ----------------------------------------------------------------
// login.ts
// ----------------------------------------------------------------
describe('login', () => {
it('sends Command_Login with plain password when no salt', () => {
@ -194,7 +193,7 @@ describe('login', () => {
});
it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => {
login({ host: 'h', port: '1', userName: 'alice', reason: Enriched.WebSocketConnectReason.LOGIN }, 'pw', 'salt');
login({ host: 'h', port: '1', userName: 'alice', reason: WebSocketConnectReason.LOGIN }, 'pw', 'salt');
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
invokeOnSuccess(loginResp, { responseCode: 0 });
const calledWith = (WebClient.instance.response.session.loginSuccessful as Mock).mock.calls[0][0];
@ -266,9 +265,6 @@ describe('login', () => {
});
});
// ----------------------------------------------------------------
// register.ts
// ----------------------------------------------------------------
describe('register', () => {
it('sends Command_Register with plain password when no salt', () => {
@ -371,9 +367,6 @@ describe('register', () => {
});
});
// ----------------------------------------------------------------
// activate.ts
// ----------------------------------------------------------------
describe('activate', () => {
it('sends Command_Activate with userName and token, not password', () => {
@ -405,9 +398,6 @@ describe('activate', () => {
});
});
// ----------------------------------------------------------------
// forgotPasswordChallenge.ts
// ----------------------------------------------------------------
describe('forgotPasswordChallenge', () => {
it('sends Command_ForgotPasswordChallenge', () => {
@ -432,9 +422,6 @@ describe('forgotPasswordChallenge', () => {
});
});
// ----------------------------------------------------------------
// forgotPasswordRequest.ts
// ----------------------------------------------------------------
describe('forgotPasswordRequest', () => {
it('sends Command_ForgotPasswordRequest', () => {
@ -470,9 +457,6 @@ describe('forgotPasswordRequest', () => {
});
});
// ----------------------------------------------------------------
// forgotPasswordReset.ts
// ----------------------------------------------------------------
describe('forgotPasswordReset', () => {
it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => {
@ -508,9 +492,6 @@ describe('forgotPasswordReset', () => {
});
});
// ----------------------------------------------------------------
// requestPasswordSalt.ts
// ----------------------------------------------------------------
describe('requestPasswordSalt', () => {
it('sends Command_RequestPasswordSalt', () => {

View file

@ -92,7 +92,6 @@ beforeEach(() => {
(passwordSaltSupported as Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
describe('accountEdit', () => {
it('sends Command_AccountEdit with correct params', () => {

View file

@ -1,4 +1,4 @@
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { WebClient } from '../../WebClient';
export function updateStatus(status: StatusEnum, description: string): void {

View file

@ -1,5 +1,5 @@
import type { Event_AttachCard } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_ChangeZoneProperties } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_CreateArrow } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_CreateCounter } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_CreateToken } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function createToken(data: Event_CreateToken, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_DelCounter } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_DeleteArrow } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_DestroyCard } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_DrawCards } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_DumpZone } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_FlipCard } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function flipCard(data: Event_FlipCard, meta: GameEventMeta): void {

View file

@ -1,4 +1,4 @@
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function gameClosed(_data: {}, meta: GameEventMeta): void {

View file

@ -1,4 +1,4 @@
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
/**

View file

@ -1,5 +1,5 @@
import type { Event_GameSay } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function gameSay(data: Event_GameSay, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_GameStateChanged } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void {

View file

@ -35,7 +35,7 @@ import {
Event_ReverseTurn_ext,
} from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { attachCard } from './attachCard';
import { changeZoneProperties } from './changeZoneProperties';

View file

@ -1,5 +1,5 @@
import type { Event_Join } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';

View file

@ -1,4 +1,4 @@
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function kicked(_data: {}, meta: GameEventMeta): void {

View file

@ -1,4 +1,4 @@
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function leaveGame(data: { reason: number }, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_MoveCard } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_PlayerPropertiesChanged } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_RevealCards } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_ReverseTurn } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_RollDie } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function rollDie(data: Event_RollDie, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_SetActivePhase } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_SetActivePlayer } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_SetCardAttr } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_SetCardCounter } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_SetCounter } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void {

View file

@ -1,5 +1,5 @@
import type { Event_Shuffle } from '@app/generated';
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../../types/WebSocketConfig';
import { WebClient } from '../../WebClient';
export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void {

View file

@ -1,11 +1,10 @@
import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { updateStatus } from '../../commands/session';
export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void {
let message: string;
// @TODO (5)
if (reasonStr) {
message = reasonStr;
} else {

View file

@ -1,9 +1,9 @@
import type { Event_ServerIdentification } from '@app/generated';
import { WebClient } from '../../WebClient';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { StatusEnum } from '../../types/StatusEnum';
import { PROTOCOL_VERSION } from '../../config';
import { consumePendingOptions } from '../../utils/connectionState';
import { WebSocketConnectReason } from '../../interfaces/ConnectOptions';
import { WebSocketConnectReason } from '../../types/ConnectOptions';
import { generateSalt, passwordSaltSupported } from '../../utils';
import * as SessionCommands from '../../commands/session';

View file

@ -58,8 +58,8 @@ import * as Config from '../../config';
import * as SessionCmds from '../../commands/session';
import { consumePendingOptions } from '../../utils/connectionState';
import { passwordSaltSupported } from '../../utils';
import { WebSocketConnectReason } from '../../interfaces/ConnectOptions';
import { StatusEnum } from '../../interfaces/StatusEnum';
import { WebSocketConnectReason } from '../../types/ConnectOptions';
import { StatusEnum } from '../../types/StatusEnum';
import { Mock } from 'vitest';
import { gameJoined } from './gameJoined';
import { notifyUser } from './notifyUser';
@ -78,9 +78,6 @@ import { serverIdentification } from './serverIdentification';
const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] };
// ----------------------------------------------------------------
// gameJoined
// ----------------------------------------------------------------
describe('gameJoined', () => {
it('calls WebClient.instance.response.session.gameJoined', () => {
@ -90,9 +87,6 @@ describe('gameJoined', () => {
});
});
// ----------------------------------------------------------------
// notifyUser
// ----------------------------------------------------------------
describe('notifyUser', () => {
it('calls WebClient.instance.response.session.notifyUser', () => {
@ -102,9 +96,6 @@ describe('notifyUser', () => {
});
});
// ----------------------------------------------------------------
// replayAdded
// ----------------------------------------------------------------
describe('replayAdded', () => {
it('calls WebClient.instance.response.session.replayAdded with matchInfo', () => {
@ -116,9 +107,6 @@ describe('replayAdded', () => {
});
});
// ----------------------------------------------------------------
// serverCompleteList
// ----------------------------------------------------------------
describe('serverCompleteList', () => {
it('calls WebClient.instance.response.session.updateUsers and WebClient.instance.response.room.updateRooms', () => {
@ -129,9 +117,6 @@ describe('serverCompleteList', () => {
});
});
// ----------------------------------------------------------------
// serverMessage
// ----------------------------------------------------------------
describe('serverMessage', () => {
it('calls WebClient.instance.response.session.serverMessage with message', () => {
@ -140,9 +125,6 @@ describe('serverMessage', () => {
});
});
// ----------------------------------------------------------------
// serverShutdown
// ----------------------------------------------------------------
describe('serverShutdown', () => {
it('calls WebClient.instance.response.session.serverShutdown', () => {
@ -152,9 +134,6 @@ describe('serverShutdown', () => {
});
});
// ----------------------------------------------------------------
// userJoined
// ----------------------------------------------------------------
describe('userJoined', () => {
it('calls WebClient.instance.response.session.userJoined with userInfo', () => {
@ -166,9 +145,6 @@ describe('userJoined', () => {
});
});
// ----------------------------------------------------------------
// userLeft
// ----------------------------------------------------------------
describe('userLeft', () => {
it('calls WebClient.instance.response.session.userLeft with name', () => {
@ -177,9 +153,6 @@ describe('userLeft', () => {
});
});
// ----------------------------------------------------------------
// userMessage
// ----------------------------------------------------------------
describe('userMessage', () => {
it('calls WebClient.instance.response.session.userMessage', () => {
@ -189,9 +162,6 @@ describe('userMessage', () => {
});
});
// ----------------------------------------------------------------
// addToList
// ----------------------------------------------------------------
describe('addToList', () => {
let logSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
@ -225,9 +195,6 @@ describe('addToList', () => {
});
});
// ----------------------------------------------------------------
// removeFromList
// ----------------------------------------------------------------
describe('removeFromList', () => {
it('buddy list → removeFromBuddyList', () => {
@ -248,9 +215,6 @@ describe('removeFromList', () => {
});
});
// ----------------------------------------------------------------
// listRooms
// ----------------------------------------------------------------
describe('listRooms', () => {
it('calls WebClient.instance.response.room.updateRooms', () => {
@ -279,9 +243,6 @@ describe('listRooms', () => {
});
});
// ----------------------------------------------------------------
// connectionClosed
// ----------------------------------------------------------------
describe('connectionClosed', () => {
it('uses reasonStr when provided', () => {
@ -371,9 +332,6 @@ describe('connectionClosed', () => {
});
});
// ----------------------------------------------------------------
// serverIdentification
// ----------------------------------------------------------------
describe('serverIdentification', () => {
const makeInfo = (overrides: Record<string, unknown> = {}) =>
create(Event_ServerIdentificationSchema, {

View file

@ -1,32 +1,10 @@
export * from './commands';
export * from './interfaces';
export { WebClient } from './WebClient';
export { StatusEnum } from './interfaces/StatusEnum';
export type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
export type {
KeyOf,
GameEventMeta,
WebSocketSessionResponseOverrides,
WebSocketRoomResponseOverrides,
} from './interfaces/WebSocketConfig';
export { SessionEvents } from './events/session';
export { RoomEvents } from './events/room';
export { GameEvents } from './events/game';
export { generateSalt, passwordSaltSupported, hashPassword } from './utils';
export { WebSocketConnectReason } from './interfaces/ConnectOptions';
export type {
LoginConnectOptions,
RegisterConnectOptions,
ActivateConnectOptions,
PasswordResetRequestConnectOptions,
PasswordResetChallengeConnectOptions,
PasswordResetConnectOptions,
TestConnectionOptions,
WebSocketConnectOptions,
} from './interfaces/ConnectOptions';
export { setPendingOptions, consumePendingOptions } from './utils/connectionState';

View file

@ -1,44 +0,0 @@
import type {
GameEventContext,
Response_Login,
Response,
Event_RoomSay,
ResponseMap,
RoomEventMap,
} from '@app/generated';
// ── KeyOf utility ────────────────────────────────────────────────────────────
// Derives a type map key from a generated type. Allows interface methods to
// reference generated types instead of hardcoded string keys.
//
// T[KeyOf<ResponseMap, Response_Login>]
// ↓ resolves to ↓
// T['Response_Login']
export type KeyOf<Map, V> = { [K in keyof Map]: Map[K] extends V ? K : never }[keyof Map];
// ── GameEventMeta ────────────────────────────────────────────────────────────
// Per-container metadata passed to every game event handler alongside the
// event payload. Constructed by ProtobufService.processGameEvent from the
// GameEventContainer fields. Structurally identical to Enriched.GameEventMeta.
export interface GameEventMeta {
gameId: number;
playerId: number;
context: GameEventContext | null;
secondsElapsed: number;
forcedByJudge: number;
}
// ── Websocket-layer enrichments ──────────────────────────────────────────────
// Protocol-level enrichments of proto types — these are websocket concerns,
// not app concerns. Used as the DEFAULT generic on the response interfaces.
export interface WebSocketSessionResponseOverrides extends ResponseMap {
Response_Login: Response_Login & { hashedPassword?: string };
Response: Response & { host: string; port: string; userName: string };
}
export interface WebSocketRoomResponseOverrides extends RoomEventMap {
Event_RoomSay: Event_RoomSay & { timeReceived: number };
}

View file

@ -510,10 +510,6 @@ describe('ProtobufService', () => {
});
// ── Real protobuf round-trip test ─────────────────────────────────────────────
// This describe block does NOT mock @bufbuild/protobuf so it exercises real
// binary serialization. It proves that the schemas ProtobufService uses
// survive a toBinary → fromBinary cycle without data loss.
describe('ProtobufService protobuf round-trip (real @bufbuild/protobuf)', () => {
it('CommandContainer round-trips cmdId through toBinary → fromBinary', async () => {
const { create, toBinary, fromBinary: realFromBinary } =

View file

@ -26,7 +26,7 @@ import {
import { GameEvents } from '../events/game';
import { RoomEvents } from '../events/room';
import { SessionEvents } from '../events/session';
import type { GameEventMeta } from '../interfaces/WebSocketConfig';
import type { GameEventMeta } from '../types/WebSocketConfig';
import { type CommandOptions, handleResponse } from './command-options';
export interface SocketTransport {

View file

@ -9,7 +9,7 @@ vi.mock('../config', () => ({
import { WebSocketService } from './WebSocketService';
import type { WebSocketServiceConfig } from './WebSocketService';
import { KeepAliveService } from './KeepAliveService';
import { StatusEnum } from '../interfaces/StatusEnum';
import { StatusEnum } from '../types/StatusEnum';
type WebSocketInternal = WebSocketService & {
keepAliveService: KeepAliveService;

View file

@ -1,9 +1,9 @@
import { Subject } from 'rxjs';
import { StatusEnum } from '../interfaces/StatusEnum';
import { StatusEnum } from '../types/StatusEnum';
import { KeepAliveService } from './KeepAliveService';
import { CLIENT_OPTIONS } from '../config';
import type { ConnectTarget } from '../interfaces/WebClientConfig';
import type { ConnectTarget } from '../types/WebClientConfig';
export interface WebSocketServiceConfig {
keepAliveFn: (pingReceived: () => void) => void;
@ -16,7 +16,7 @@ export class WebSocketService {
private config: WebSocketServiceConfig;
private keepAliveService: KeepAliveService;
private errorFired = false;
private hasReportedError = false;
public message$: Subject<MessageEvent> = new Subject();
@ -68,7 +68,7 @@ export class WebSocketService {
socket.onopen = () => {
clearTimeout(connectionTimer);
this.errorFired = false;
this.hasReportedError = false;
this.config.onStatusChange(StatusEnum.CONNECTED, 'Connected');
this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: () => void) => {
@ -77,16 +77,17 @@ export class WebSocketService {
};
socket.onclose = () => {
// dont overwrite failure messages
if (!this.errorFired) {
// @critical onerror + onclose both fire on failed connects; don't overwrite the richer error status.
// See .github/instructions/webclient.instructions.md#websocket-lifecycle.
if (!this.hasReportedError) {
this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Closed');
}
this.errorFired = false;
this.hasReportedError = false;
this.keepAliveService.endPingLoop();
};
socket.onerror = () => {
this.errorFired = true;
this.hasReportedError = true;
this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Failed');
this.config.onConnectionFailed();
};

View file

@ -10,12 +10,6 @@ import { create, getExtension } from '@bufbuild/protobuf';
import { handleResponse } from './command-options';
// NOTE: do NOT call `vi.resetAllMocks()` here — under `isolate: false` it
// resets `vi.fn()` implementations set inside other files' `vi.mock(...)`
// factories, which breaks any spec that relied on those factory defaults
// (e.g. ProtobufService.spec.ts expects `hasExtension` to return `false`).
// The root `setupTests.ts` afterEach already calls `vi.clearAllMocks()`.
describe('handleResponse', () => {
it('calls onResponse and returns early when provided', () => {
const onResponse = vi.fn();

View file

@ -10,12 +10,6 @@ export enum WebSocketConnectReason {
TEST_CONNECTION,
}
// ── Connect options ───────────────────────────────────────────────────────────
// Each variant is the enriched input for one session flow: the network
// transport fields (host/port) + the subset of proto Command_* fields the UI
// actually produces (user-entered credentials, tokens, email, etc.) + a
// `reason` discriminator so the websocket layer can route.
interface ConnectTransport extends ConnectTarget {
keepalive?: number;
autojoinrooms?: boolean;

View file

@ -0,0 +1,17 @@
/**
* Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the
* activation dialog can resubmit against the same host/user without re-entering them.
*/
export interface PendingActivationContext {
host: string;
port: string;
userName: string;
}
/**
* Payload for the LOGIN_SUCCESSFUL signal. Only carries what the UI needs to
* persist into the selected host record (hashedPassword for "remember me").
*/
export interface LoginSuccessContext {
hashedPassword?: string;
}

View file

@ -39,7 +39,6 @@ import type {
import type { ConnectTarget } from './WebClientConfig';
import type { KeyOf } from './WebSocketConfig';
// ── Auth request type map ────────────────────────────────────────────────────
// Keys = generated *Params type names composed with ConnectTarget.
// @app/api overrides these with Enriched connect option types.

View file

@ -0,0 +1,28 @@
import type {
GameEventContext,
Response_Login,
Response,
Event_RoomSay,
ResponseMap,
RoomEventMap,
} from '@app/generated';
// `KeyOf<ResponseMap, Response_Login>` resolves to `'Response_Login'`.
export type KeyOf<Map, V> = { [K in keyof Map]: Map[K] extends V ? K : never }[keyof Map];
export interface GameEventMeta {
gameId: number;
playerId: number;
context: GameEventContext | null;
secondsElapsed: number;
forcedByJudge: number;
}
export interface WebSocketSessionResponseOverrides extends ResponseMap {
Response_Login: Response_Login & { hashedPassword?: string };
Response: Response & { host: string; port: string; userName: string };
}
export interface WebSocketRoomResponseOverrides extends RoomEventMap {
Event_RoomSay: Event_RoomSay & { timeReceived: number };
}

View file

@ -0,0 +1 @@
export * as WebsocketTypes from './namespace';

View file

@ -21,3 +21,5 @@ export type {
export * from './WebClientConfig';
export * from './WebSocketConfig';
export * from './StatusEnum';
export * from './ConnectOptions';
export * from './SignalContexts';

View file

@ -1,4 +1,4 @@
import type { WebSocketConnectOptions } from '../interfaces/ConnectOptions';
import type { WebSocketConnectOptions } from '../types/ConnectOptions';
let pendingOptions: WebSocketConnectOptions | null = null;

View file

@ -8,7 +8,6 @@ const SALT_LENGTH = 16;
export const hashPassword = (salt: string, password: string): string => {
let hashedPassword = salt + password;
for (let i = 0; i < HASH_ROUNDS; i++) {
// WHY DO WE DO IT THIS WAY?
hashedPassword = sha512(hashedPassword);
}
@ -27,6 +26,6 @@ export const generateSalt = (): string => {
}
export const passwordSaltSupported = (serverOptions: number): number => {
// Intentional use of Bitwise operator b/c of how Servatrice Enums work
// @critical Servatrice ServerOptions is a bitmask. See .github/instructions/webclient.instructions.md#protocol-quirks.
return serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash;
}