The store that had never been tested

pvpGameState.svelte.ts had never had a single unit test of its own. Turning it into an injectable factory meant answering, before touching it, whether Svelte 5 runes would even tolerate the shape.

pvpGameState.svelte.ts has held the PvP store since the duel shipped, and in all that time it had never had a unit test that exercised it directly, only the end-to-end suite watching it through a page. PvpSyncEngine already took the sync and transport logic out of the store and made all of that testable behind a fake PvpSyncApi, but the store itself still called new EventSource(...) and touched localStorage inline, both of which meant a real browser to run at all.

The question worth resolving before doing anything was whether wrapping the whole store in a factory would even work. Svelte 5’s runes are normally written inside a component; $state and $derived called from a plain function, invoked more than once outside any component, was untested territory for this codebase. Rather than start the refactor and find out mid-way, a throwaway spike answered it first: a branch converting the store into createPvpGameStore(deps), invoked twice in the same process, one production instance and one for a test.

The runes turned out not to be the risk at all. Both instances compiled and behaved correctly, no rune-specific errors anywhere, and the store uses no $effect that could have depended on component lifecycle. The only real friction was mundane DOM typing, the real EventSource’s onmessage carrying a far wider type than the store actually reads, fixed with one documented cast at the default factory boundary. With the spike proving the seam and a separate check confirming the injection point didn’t conflict with the SSE architecture decisions already on record, converting the store for real was a locked decision rather than an open question by the time it started.

/** Injectable dependencies for the store. `api`/`clock` pass straight
 *  through to PvpSyncEngine, which was already injectable — this store
 *  itself only needed an EventSource constructor and a storage abstraction
 *  to become fully testable. */
export interface PvpGameStoreDeps {
	createEventSource?: (url: string) => EventSourceLike;
	storage?: Storage;
	api?: PvpSyncApi;
	clock?: Clock;
}

export function createPvpGameStore(deps: PvpGameStoreDeps = {}) {

export const pvpGame = createPvpGameStore() sits at the bottom of the file as the real production singleton, so every existing caller, the layout and the PvP page, needed zero changes. The fake storage a test can supply throws loudly instead of quietly returning undefined if it’s ever reached outside a browser guard, catching a class of SSR bug at the moment it happens rather than downstream as a mysterious null.

The conversion surfaced one bug the store had been carrying by accident: its own now() and the Clock object passed into createPvpSyncEngine were built independently, two separate Date.now references that happened to agree only because nothing had ever made them disagree. Threading a single Clock through both call sites turned that coincidence into a guarantee. pvpGameState.svelte.test.ts is new, and covers the outcome-application quartet through fake transports, a rejected word never reaching it, and leave() tearing down one instance without touching a sibling, none of which had a home to be tested in before this file had a seam to test through.