Skip to content

Commit 268600e

Browse files
falucianoclaude
andcommitted
fix(test): type the projection fixture instead of casting past the checker
CI caught two type errors I introduced and did not see: I ran the typecheck before adding the test file and never re-ran it. The fixture cast a hand-built state to HostGameState, which papered over Card's branded CardInstanceId. Builds properly typed cards through a small factory instead, so the fixture is checked rather than asserted. The Record cast now goes through unknown, which is what that widening actually requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 84fa232 commit 268600e

1 file changed

Lines changed: 23 additions & 17 deletions

File tree

packages/shared/src/bridge/__tests__/client-view.test.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
22
import { createHostInitialState, hostReducer } from "../host-reducer";
33
import { createHostClientView } from "../client-view";
44
import type { HostGameState } from "../host-state";
5-
import type { CardGameRuleset } from "../../types/index";
5+
import type { Card, CardGameRuleset, CardInstanceId } from "../../types/index";
66

77
/**
88
* These tests are the guarantee behind server-side projection: a player's
@@ -59,29 +59,32 @@ function makeStartedGameState(): HostGameState {
5959
// hand would make these assertions pass without proving anything.
6060
const engineState = state.engineState;
6161
if (!engineState) throw new Error("expected a started game");
62+
63+
const alicesHand = [
64+
card("ALICE-CARD-1", "A", "spades"),
65+
card("ALICE-CARD-2", "K", "hearts"),
66+
];
67+
const bobsHand = [
68+
card("BOB-SECRET-1", "Q", "clubs"),
69+
card("BOB-SECRET-2", "J", "diamonds"),
70+
];
71+
6272
return {
6373
...state,
6474
engineState: {
6575
...engineState,
6676
zones: {
6777
...engineState.zones,
68-
"hand:0": {
69-
...engineState.zones["hand:0"],
70-
cards: [
71-
{ id: "ALICE-CARD-1", rank: "A", suit: "spades", faceUp: false },
72-
{ id: "ALICE-CARD-2", rank: "K", suit: "hearts", faceUp: false },
73-
],
74-
},
75-
"hand:1": {
76-
...engineState.zones["hand:1"],
77-
cards: [
78-
{ id: "BOB-SECRET-1", rank: "Q", suit: "clubs", faceUp: false },
79-
{ id: "BOB-SECRET-2", rank: "J", suit: "diamonds", faceUp: false },
80-
],
81-
},
78+
"hand:0": { ...engineState.zones["hand:0"]!, cards: alicesHand },
79+
"hand:1": { ...engineState.zones["hand:1"]!, cards: bobsHand },
8280
},
8381
},
84-
} as HostGameState;
82+
};
83+
}
84+
85+
/** A card with the branded instance id the engine expects. */
86+
function card(id: string, rank: Card["rank"], suit: Card["suit"]): Card {
87+
return { id: id as CardInstanceId, rank, suit, faceUp: false };
8588
}
8689

8790
/** Card ids in a player's hand, read from the authoritative state. */
@@ -119,7 +122,10 @@ describe("createHostClientView", () => {
119122

120123
it("omits the engine state entirely", () => {
121124
const state = makeStartedGameState();
122-
const view = createHostClientView(state, "p1") as Record<string, unknown>;
125+
const view = createHostClientView(state, "p1") as unknown as Record<
126+
string,
127+
unknown
128+
>;
123129

124130
// The controller works from `playerView`; raw engine state is the thing
125131
// that leaked, so it must not survive projection under any key.

0 commit comments

Comments
 (0)