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.

flowchart TD
  accTitle: PvP store factory with injectable dependencies
  accDescr: The createPvpGameStore factory accepts four injectable dependencies: createEventSource for the EventSource constructor, storage for localStorage abstraction, api for PvpSyncApi already used by PvpSyncEngine, and clock for time operations. The production singleton at module bottom calls it with no args using browser defaults. Tests supply fake implementations for each.
  
  prodEventSource["Browser EventSource<br/>(default)"]
  prodStorage["Browser localStorage<br/>(default)"]
  prodApi["PvpSyncApi"]
  prodClock["Clock"]
  
  testEventSource["Fake EventSource<br/>(test)"]
  testStorage["Fake Storage<br/>(test)"]
  testApi["Fake PvpSyncApi<br/>(test)"]
  testClock["Fake Clock<br/>(test)"]
  
  prodEventSource --> factory["createPvpGameStore<br/>(deps)"]
  prodStorage --> factory
  prodApi --> factory
  prodClock --> factory
  
  testEventSource -.->|test instance| factory
  testStorage -.->|test instance| factory
  testApi -.->|test instance| factory
  testClock -.->|test instance| factory
  
  factory --> pvpGame["export const pvpGame<br/>=<br/>createPvpGameStore()"]
  factory -->|for test| testStore["Test store instances"]
  
  class factory,pvpGame accent
createPvpGameStore(deps) factory accepts four injectable dependencies: EventSource constructor, Storage abstraction, PvpSyncApi, and Clock. The production singleton invokes it with defaults; tests supply fakes.

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.