@eigeninteractive/testkit 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,2 @@
1
1
  #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Emits `game-contract.json` from an EigenInteractive package's conventional layout.
3
+ *
4
+ * This is the programmatic form of the `eigen-contract` executable. Most
5
+ * games should invoke the executable through their package script.
6
+ */
7
+ export declare function emitConfiguredGameContract(root?: string): Promise<void>;
8
+ /**
9
+ * Fails when the conventionally configured contract is absent or stale.
10
+ *
11
+ * Use this in CI through `eigen-contract --check`.
12
+ */
13
+ export declare function checkConfiguredGameContract(root?: string): Promise<void>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Emit the language-neutral contract a game's Flutter repository consumes.
3
+ *
4
+ * The authoritative inputs live with the TypeScript rules: the four payload
5
+ * schemas and the shared behavioral fixtures. The output is one deterministic
6
+ * JSON file so it can be copied, attached to a release, or fetched by checksum
7
+ * without assuming any repository layout.
8
+ */
9
+ import { type GameModule } from "@eigeninteractive/rules";
10
+ /** Current format of the language-neutral contract consumed by EigenInteractive's Dart generator. */
11
+ export declare const GAME_CONTRACT_FORMAT_VERSION = 1;
12
+ /** One validated twin-fixture document embedded in a {@link GameContract}. */
13
+ export interface GameContractFixture {
14
+ /** POSIX-style path relative to the supplied fixtures root. */
15
+ path: string;
16
+ /** Validated fixture document, retained in its original JSON shape. */
17
+ document: unknown;
18
+ }
19
+ /** The four JSON Schemas emitted for one game `schemaVersion`. */
20
+ export interface GameContractVersion {
21
+ schemas: {
22
+ state: Record<string, unknown>;
23
+ observation: Record<string, unknown>;
24
+ action: Record<string, unknown>;
25
+ config: Record<string, unknown>;
26
+ };
27
+ }
28
+ /** Language-neutral schemas and fixtures shared by a game's Worker and app. */
29
+ export interface GameContract {
30
+ formatVersion: typeof GAME_CONTRACT_FORMAT_VERSION;
31
+ game: string;
32
+ versions: Record<string, GameContractVersion>;
33
+ fixtures: GameContractFixture[];
34
+ }
35
+ /** Inputs for building a {@link GameContract} without writing it. */
36
+ export interface BuildGameContractOptions {
37
+ /** Stable display name used as the generated Dart type prefix. */
38
+ game: string;
39
+ /** Authoritative TypeScript rules registry. */
40
+ gameModule: GameModule;
41
+ /** Root containing `v<N>/*.json` twin fixtures. */
42
+ fixturesRoot?: string | URL;
43
+ }
44
+ /** Inputs for emitting or checking a {@link GameContract} file. */
45
+ export interface EmitGameContractOptions extends BuildGameContractOptions {
46
+ /** Destination `game-contract.json` path. */
47
+ output: string | URL;
48
+ }
49
+ /** Build a deterministic in-memory contract without touching the filesystem. */
50
+ export declare function buildGameContract(options: BuildGameContractOptions): GameContract;
51
+ /** Render one deterministic, newline-terminated contract document. */
52
+ export declare function gameContractJson(options: BuildGameContractOptions): string;
53
+ /** Emit one deterministic, newline-terminated `game-contract.json`. */
54
+ export declare function emitGameContract(options: EmitGameContractOptions): void;
55
+ /** Fail when an emitted contract is missing or differs from its inputs. */
56
+ export declare function checkGameContract(options: EmitGameContractOptions): void;
57
+ /** A useful default filename for scripts that accept an output directory. */
58
+ export declare const gameContractFilename: (game: string) => string;
package/dist/index.d.ts CHANGED
@@ -1,249 +1,11 @@
1
- import { GameModule, GameRules, JsonObject, TransitionCause, OutcomeEntry, GameAccess, Json } from '@eigeninteractive/rules';
2
- import { SeatView } from '@eigeninteractive/kernel';
3
- export { CommitInput, CommitPlan, DEADLINE_GRACE_MS, Effect, GameRow, Intent, ObservationFrame, RejectCode, Rejected, Seat, SeatView, StateRow, commit, deriveRng, isRejected, randomSeed } from '@eigeninteractive/kernel';
4
-
5
1
  /**
6
- * Emits `game-contract.json` from an EigenInteractive package's conventional layout.
2
+ * `@eigeninteractive/testkit`: drive a game's rules through the real kernel without a
3
+ * Worker, a database or a network. Build a table, submit actions as seats,
4
+ * assert on the resulting transitions and per-seat observations.
7
5
  *
8
- * This is the programmatic form of the `eigen-contract` executable. Most
9
- * games should invoke the executable through their package script.
6
+ * @module @eigeninteractive/testkit
10
7
  */
11
- declare function emitConfiguredGameContract(root?: string): Promise<void>;
12
- /**
13
- * Fails when the conventionally configured contract is absent or stale.
14
- *
15
- * Use this in CI through `eigen-contract --check`.
16
- */
17
- declare function checkConfiguredGameContract(root?: string): Promise<void>;
18
-
19
- /**
20
- * Emit the language-neutral contract a game's Flutter repository consumes.
21
- *
22
- * The authoritative inputs live with the TypeScript rules: the four payload
23
- * schemas and the shared behavioral fixtures. The output is one deterministic
24
- * JSON file so it can be copied, attached to a release, or fetched by checksum
25
- * without assuming any repository layout.
26
- */
27
-
28
- /** Current format of the language-neutral contract consumed by EigenInteractive's Dart generator. */
29
- declare const GAME_CONTRACT_FORMAT_VERSION = 1;
30
- /** One validated twin-fixture document embedded in a {@link GameContract}. */
31
- interface GameContractFixture {
32
- /** POSIX-style path relative to the supplied fixtures root. */
33
- path: string;
34
- /** Validated fixture document, retained in its original JSON shape. */
35
- document: unknown;
36
- }
37
- /** The four JSON Schemas emitted for one game `schemaVersion`. */
38
- interface GameContractVersion {
39
- schemas: {
40
- state: Record<string, unknown>;
41
- observation: Record<string, unknown>;
42
- action: Record<string, unknown>;
43
- config: Record<string, unknown>;
44
- };
45
- }
46
- /** Language-neutral schemas and fixtures shared by a game's Worker and app. */
47
- interface GameContract {
48
- formatVersion: typeof GAME_CONTRACT_FORMAT_VERSION;
49
- game: string;
50
- versions: Record<string, GameContractVersion>;
51
- fixtures: GameContractFixture[];
52
- }
53
- /** Inputs for building a {@link GameContract} without writing it. */
54
- interface BuildGameContractOptions {
55
- /** Stable display name used as the generated Dart type prefix. */
56
- game: string;
57
- /** Authoritative TypeScript rules registry. */
58
- gameModule: GameModule;
59
- /** Root containing `v<N>/*.json` twin fixtures. */
60
- fixturesRoot?: string | URL;
61
- }
62
- /** Inputs for emitting or checking a {@link GameContract} file. */
63
- interface EmitGameContractOptions extends BuildGameContractOptions {
64
- /** Destination `game-contract.json` path. */
65
- output: string | URL;
66
- }
67
- /** Build a deterministic in-memory contract without touching the filesystem. */
68
- declare function buildGameContract(options: BuildGameContractOptions): GameContract;
69
- /** Render one deterministic, newline-terminated contract document. */
70
- declare function gameContractJson(options: BuildGameContractOptions): string;
71
- /** Emit one deterministic, newline-terminated `game-contract.json`. */
72
- declare function emitGameContract(options: EmitGameContractOptions): void;
73
- /** Fail when an emitted contract is missing or differs from its inputs. */
74
- declare function checkGameContract(options: EmitGameContractOptions): void;
75
- /** A useful default filename for scripts that accept an output directory. */
76
- declare const gameContractFilename: (game: string) => string;
77
-
78
- /**
79
- * The kernel-scenario surface for game tests: how an implementor drives real
80
- * engine transitions over their rules unit (same-view pairs, timing/grace
81
- * behavior, lifecycle outcomes) without depending on the engine's internals.
82
- *
83
- * `@eigeninteractive/kernel` is an internal package: implementors ship `@eigeninteractive/rules` +
84
- * `@eigeninteractive/server` and dev-depend on `@eigeninteractive/testkit` alone. Everything a
85
- * game's test suite legitimately needs from the kernel is re-exported here,
86
- * so the kernel's own surface stays free to churn.
87
- */
88
-
89
- /** Project one seat's view of a state: the stored-frame shape the same-view
90
- * rule compares (`commit()`'s `staleViews` input). Convenience for scenario
91
- * tests that replay a simultaneous-move race. */
92
- declare function projectView(rules: GameRules, args: {
93
- state: JsonObject;
94
- pending: number[];
95
- /** The seat to project for, or null for a viewer. */
96
- seat: number | null;
97
- config: JsonObject;
98
- cause?: TransitionCause;
99
- participantCount?: number;
100
- isReplay?: boolean;
101
- }): SeatView;
102
-
103
- /**
104
- * Twin-drift fixture runner: the TS half of the shared JSON fixtures that
105
- * keep a version unit's TS and Dart `GameRules` twins in sync. The JSON
106
- * fixture format is shared verbatim with the Dart runner
107
- * (`lib/testing/twin_fixtures.dart`): one file, two consumers.
108
- *
109
- * One fixture file per concern lives beside the version units at
110
- * `<fixturesRoot>/v<N>/*.json` and is consumed by BOTH sides: this module
111
- * runs each case against the TS unit (schemas + `applyAction` +
112
- * `computeObservation` + the two predicates), while the Dart runner runs the
113
- * same file against the Dart twin (generated payload parsing,
114
- * `isValidAction`, `previewAction`, predicate twins). A behavioral divergence
115
- * then fails one side's CI instead of degrading UX in production.
116
- *
117
- * ## Fixture file format
118
- *
119
- * ```jsonc
120
- * {
121
- * "schemaVersion": 1,
122
- * "cases": [
123
- * {
124
- * "kind": "action",
125
- * "name": "seat 0 marks an empty cell",
126
- * "config": {},
127
- * "state": { ... }, // TS: applyAction input
128
- * "obs": { ... }, // Dart-only; defaults to `state`
129
- * "pending": [0],
130
- * "playerIndex": 0,
131
- * "participantCount": 2, // optional, default 2
132
- * "rngSeed": "any string", // optional, default "twin-fixtures"
133
- * "action": { ... },
134
- * "expected": {
135
- * "valid": true, // false ⇒ TS IllegalMoveError,
136
- * // Dart isValidAction false
137
- * "state": { ... }, // optional, TS envelope.state
138
- * "pending": [1], // optional, TS envelope.pendingPlayers
139
- * "outcome": [ ... ], // optional, TS envelope.outcome
140
- * // (null asserts the game is ongoing)
141
- * "observation": { ... } // optional, the actor's post-action
142
- * // view: TS computeObservation slice
143
- * // data; Dart previewAction (when the
144
- * // game implements optimism)
145
- * }
146
- * },
147
- * { "kind": "playerLimits", "name": "...", "config": {},
148
- * "expected": { "minPlayers": 2, "maxPlayers": 2 } },
149
- * { "kind": "ratingPool", "name": "...", "access": "public",
150
- * "minPlayers": 2, "maxPlayers": 2, "config": {}, "expected": "blitz" },
151
- * { "kind": "botSeatable", "name": "...", "gameConfig": {},
152
- * "botConfig": {}, "expected": false }
153
- * ]
154
- * }
155
- * ```
156
- *
157
- * The `state`/`obs` split exists for hidden-info games (a seat's observation
158
- * is not the state); perfect-info games omit `obs`. `expected.observation` is
159
- * the shared behavioral anchor: the TS side must project the post-action
160
- * state to it, and a Dart `previewAction` that returns non-null must predict
161
- * it, so the two sides are compared through one recorded value.
162
- *
163
- * Wire it up in a game-owned test file running under plain-Node vitest:
164
- *
165
- * ```ts
166
- * import { twinFixtureTests } from "@eigeninteractive/testkit";
167
- * import gameModule from "../src/module/index.js";
168
- *
169
- * twinFixtureTests(gameModule, new URL("../src/module/fixtures/", import.meta.url));
170
- * ```
171
- */
172
-
173
- /** One fixture file: cases targeting one `schemaVersion` unit. */
174
- interface TwinFixtureFile {
175
- schemaVersion: number;
176
- cases: TwinFixtureCase[];
177
- }
178
- /** A game-action case: exercises schemas, `applyAction`, and (through
179
- * `expected.observation`) `computeObservation` for the acting seat. */
180
- interface ActionCase {
181
- kind: "action";
182
- name: string;
183
- config: JsonObject;
184
- state: JsonObject;
185
- /** Dart-side observation payload; unused here (defaults to `state`). */
186
- obs?: JsonObject;
187
- pending: number[];
188
- playerIndex: number;
189
- participantCount?: number;
190
- rngSeed?: string;
191
- action: JsonObject;
192
- expected: {
193
- valid: boolean;
194
- state?: JsonObject;
195
- pending?: number[];
196
- outcome?: OutcomeEntry[] | null;
197
- observation?: JsonObject;
198
- };
199
- }
200
- /** A `ratingPool` predicate case. Omitted timing fields mean null. */
201
- interface RatingPoolCase {
202
- kind: "ratingPool";
203
- name: string;
204
- access: GameAccess;
205
- turnSeconds?: number | null;
206
- budgetSeconds?: number | null;
207
- incrementSeconds?: number | null;
208
- minPlayers: number;
209
- maxPlayers: number;
210
- config: JsonObject;
211
- expected: string | null;
212
- }
213
- /** A `playerLimits` case: the seats one config may be played with. */
214
- interface PlayerLimitsCase {
215
- kind: "playerLimits";
216
- name: string;
217
- config: JsonObject;
218
- expected: {
219
- minPlayers: number;
220
- maxPlayers: number;
221
- };
222
- }
223
- /** A `botSeatable` predicate case. */
224
- interface BotSeatableCase {
225
- kind: "botSeatable";
226
- name: string;
227
- gameConfig: JsonObject;
228
- botConfig: JsonObject;
229
- expected: boolean;
230
- }
231
- type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase;
232
- /** Validate one fixture file's parsed JSON, or throw naming the offending
233
- * file, case, and field. Exported so a repo can lint its fixtures without
234
- * running them. */
235
- declare function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile;
236
- /** Run one fixture case against a rules unit, returning failure descriptions
237
- * (empty ⇒ the case passes). Pure; the file-reading test registrar is
238
- * {@link twinFixtureTests}. */
239
- declare function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase): string[];
240
- /** Register one vitest test per fixture case found under `fixturesRoot`
241
- * (layout: `<root>/v<N>/*.json`). Call at the top level of a test module
242
- * running in a Node environment. */
243
- declare function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void;
244
- /** Structural JSON equality. Object keys with `undefined` values count as
245
- * absent (matching how schema libraries model optional fields); array order
246
- * matters. */
247
- declare function deepEquals(a: Json | undefined, b: Json | undefined): boolean;
248
-
249
- export { type ActionCase, type BotSeatableCase, type BuildGameContractOptions, type EmitGameContractOptions, GAME_CONTRACT_FORMAT_VERSION, type GameContract, type GameContractFixture, type GameContractVersion, type PlayerLimitsCase, type RatingPoolCase, type TwinFixtureCase, type TwinFixtureFile, buildGameContract, checkConfiguredGameContract, checkGameContract, deepEquals, emitConfiguredGameContract, emitGameContract, evaluateTwinCase, gameContractFilename, gameContractJson, parseTwinFixtureFile, projectView, twinFixtureTests };
8
+ export { checkConfiguredGameContract, emitConfiguredGameContract } from "./contract-command.js";
9
+ export { type BuildGameContractOptions, buildGameContract, checkGameContract, type EmitGameContractOptions, emitGameContract, GAME_CONTRACT_FORMAT_VERSION, type GameContract, type GameContractFixture, type GameContractVersion, gameContractFilename, gameContractJson, } from "./game-contract.js";
10
+ export { type CommitInput, type CommitPlan, commit, DEADLINE_GRACE_MS, deriveRng, type Effect, type GameRow, type Intent, isRejected, type ObservationFrame, projectView, type RejectCode, type Rejected, randomSeed, type Seat, type SeatView, type StateRow, } from "./kernel-scenarios.js";
11
+ export { type ActionCase, type BotSeatableCase, deepEquals, evaluateTwinCase, type PlayerLimitsCase, parseTwinFixtureFile, type RatingPoolCase, type TwinFixtureCase, type TwinFixtureFile, twinFixtureTests, } from "./twin-fixtures.js";
@@ -1 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * `eigen-inspect`: read a Worker's local `.wrangler` state from the terminal.
4
+ *
5
+ * Deliberately narrow. Wrangler already ships a first-party browser for local
6
+ * resources (press `e` in `wrangler dev`, or open `/cdn-cgi/explorer`), and
7
+ * generic table browsing is its job, not this tool's. What that view cannot do
8
+ * is answer a question in the engine's own terms, because a game's truth is
9
+ * split across two stores on purpose: the D1 index and the game's own Durable
10
+ * Object. This reads both, decodes the JSON columns, prints the transition log
11
+ * as a timeline, and says what the game is waiting for.
12
+ *
13
+ * It also runs with no dev server, no browser and no account, and emits
14
+ * `--json` for a script or an agent.
15
+ */
16
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The kernel-scenario surface for game tests: how an implementor drives real
3
+ * engine transitions over their rules unit (same-view pairs, timing/grace
4
+ * behavior, lifecycle outcomes) without depending on the engine's internals.
5
+ *
6
+ * `@eigeninteractive/kernel` is an internal package: implementors ship `@eigeninteractive/rules` +
7
+ * `@eigeninteractive/server` and dev-depend on `@eigeninteractive/testkit` alone. Everything a
8
+ * game's test suite legitimately needs from the kernel is re-exported here,
9
+ * so the kernel's own surface stays free to churn.
10
+ */
11
+ import { type SeatView } from "@eigeninteractive/kernel";
12
+ import type { GameRules, JsonObject, TransitionCause } from "@eigeninteractive/rules";
13
+ export type { CommitInput, CommitPlan, Effect, GameRow, Intent, ObservationFrame, RejectCode, Rejected, Seat, SeatView, StateRow, } from "@eigeninteractive/kernel";
14
+ export { commit, DEADLINE_GRACE_MS, deriveRng, isRejected, randomSeed, } from "@eigeninteractive/kernel";
15
+ /** Project one seat's view of a state: the stored-frame shape the same-view
16
+ * rule compares (`commit()`'s `staleViews` input). Convenience for scenario
17
+ * tests that replay a simultaneous-move race. */
18
+ export declare function projectView(rules: GameRules, args: {
19
+ state: JsonObject;
20
+ pending: number[];
21
+ /** The seat to project for, or null for a viewer. */
22
+ seat: number | null;
23
+ config: JsonObject;
24
+ cause?: TransitionCause;
25
+ participantCount?: number;
26
+ isReplay?: boolean;
27
+ }): SeatView;
@@ -1,5 +1,3 @@
1
- import { DatabaseSync } from 'node:sqlite';
2
-
3
1
  /**
4
2
  * Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the
5
3
  * per-game Durable Object databases, opened as plain SQLite files.
@@ -34,9 +32,9 @@ import { DatabaseSync } from 'node:sqlite';
34
32
  *
35
33
  * @module @eigeninteractive/testkit/local-state
36
34
  */
37
-
35
+ import { DatabaseSync } from "node:sqlite";
38
36
  /** A local Durable Object database: one game's whole session. */
39
- interface LocalDurableObject {
37
+ export interface LocalDurableObject {
40
38
  /** The Durable Object class, parsed from the directory Miniflare names
41
39
  * `<worker>-<ClassName>`. */
42
40
  className: string;
@@ -53,17 +51,17 @@ interface LocalDurableObject {
53
51
  /** What a bound parameter may be: SQLite's own value domain, spelled out here
54
52
  * rather than re-exported from `node:sqlite` so the public signature does not
55
53
  * depend on a built-in module's type names. */
56
- type LocalQueryParam = string | number | bigint | null | Uint8Array;
54
+ export type LocalQueryParam = string | number | bigint | null | Uint8Array;
57
55
  /** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is
58
56
  * its turn deadline plus grace. An absent row means no timer is armed, which is
59
57
  * correct for an untimed game and a bug for a timed one mid-turn. */
60
- interface LocalAlarm {
58
+ export interface LocalAlarm {
61
59
  actorId: string;
62
60
  actorName: string | null;
63
61
  scheduledTime: number | null;
64
62
  }
65
63
  /** The engine's `games` row: the D1 index entry, a display mirror of the DO. */
66
- interface LocalGameRow {
64
+ export interface LocalGameRow {
67
65
  id: string;
68
66
  createdBy: string | null;
69
67
  status: string;
@@ -89,7 +87,7 @@ interface LocalGameRow {
89
87
  }
90
88
  /** One seat, from either store. `username` is filled in from D1's `users` and
91
89
  * `bots` tables when the reader has an index to join against. */
92
- interface LocalSeat {
90
+ export interface LocalSeat {
93
91
  playerIndex: number;
94
92
  userId: string | null;
95
93
  botId: string | null;
@@ -98,7 +96,7 @@ interface LocalSeat {
98
96
  displayName: string | null;
99
97
  }
100
98
  /** The DO's single `meta` row: the authoritative session header. */
101
- interface LocalGameMeta {
99
+ export interface LocalGameMeta {
102
100
  gameId: string;
103
101
  status: string;
104
102
  access: string;
@@ -115,7 +113,7 @@ interface LocalGameMeta {
115
113
  rngSeed: string | null;
116
114
  }
117
115
  /** One committed transition, decoded. `action` is null at v0 (the start). */
118
- interface LocalTransition {
116
+ export interface LocalTransition {
119
117
  version: number;
120
118
  state: unknown;
121
119
  action: {
@@ -137,7 +135,7 @@ interface LocalTransition {
137
135
  /** Why the game is where it is, and what would move it. Derived rather than
138
136
  * stored: this is the sentence a developer actually wants, and every input to
139
137
  * it is already on screen above it. */
140
- interface LocalGameDiagnosis {
138
+ export interface LocalGameDiagnosis {
141
139
  /** One line naming the next expected event, or the reason there is none. */
142
140
  summary: string;
143
141
  /** Disagreements between the D1 mirror and the DO's own truth. A non-empty
@@ -145,7 +143,7 @@ interface LocalGameDiagnosis {
145
143
  mirrorDrift: string[];
146
144
  }
147
145
  /** Everything both stores know about one game. */
148
- interface LocalGameView {
146
+ export interface LocalGameView {
149
147
  gameId: string;
150
148
  /** The D1 index row, or null when D1 has no such game: either the id came
151
149
  * from a DO name and the index was wiped, or the create write never landed. */
@@ -182,7 +180,7 @@ interface LocalGameView {
182
180
  * inside a game repository, including the app half of a two-package layout,
183
181
  * where the state lives a directory or two above the shell's cwd.
184
182
  */
185
- declare function findWranglerState(from?: string): string | null;
183
+ export declare function findWranglerState(from?: string): string | null;
186
184
  /**
187
185
  * Open a SQLite file for reading only.
188
186
  *
@@ -196,7 +194,7 @@ declare function findWranglerState(from?: string): string | null;
196
194
  * because that fallback would otherwise CREATE a database rather than report a
197
195
  * missing one.
198
196
  */
199
- declare function openSqlite(file: string): DatabaseSync;
197
+ export declare function openSqlite(file: string): DatabaseSync;
200
198
  /**
201
199
  * A Worker's local state, with its databases opened lazily and held open.
202
200
  *
@@ -204,7 +202,7 @@ declare function openSqlite(file: string): DatabaseSync;
204
202
  * {@link close} it. A single instance is the unit both the CLI and a test use,
205
203
  * so neither has to know which file backs which resource.
206
204
  */
207
- declare class LocalStore {
205
+ export declare class LocalStore {
208
206
  #private;
209
207
  /** Absolute path to `.wrangler/state/v3`. */
210
208
  readonly root: string;
@@ -277,5 +275,3 @@ declare class LocalStore {
277
275
  /** Release every open handle. */
278
276
  close(): void;
279
277
  }
280
-
281
- export { type LocalAlarm, type LocalDurableObject, type LocalGameDiagnosis, type LocalGameMeta, type LocalGameRow, type LocalGameView, type LocalQueryParam, type LocalSeat, LocalStore, type LocalTransition, findWranglerState, openSqlite };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Twin-drift fixture runner: the TS half of the shared JSON fixtures that
3
+ * keep a version unit's TS and Dart `GameRules` twins in sync. The JSON
4
+ * fixture format is shared verbatim with the Dart runner
5
+ * (`lib/testing/twin_fixtures.dart`): one file, two consumers.
6
+ *
7
+ * One fixture file per concern lives beside the version units at
8
+ * `<fixturesRoot>/v<N>/*.json` and is consumed by BOTH sides: this module
9
+ * runs each case against the TS unit (schemas + `applyAction` +
10
+ * `computeObservation` + the two predicates), while the Dart runner runs the
11
+ * same file against the Dart twin (generated payload parsing,
12
+ * `isValidAction`, `previewAction`, predicate twins). A behavioral divergence
13
+ * then fails one side's CI instead of degrading UX in production.
14
+ *
15
+ * ## Fixture file format
16
+ *
17
+ * ```jsonc
18
+ * {
19
+ * "schemaVersion": 1,
20
+ * "cases": [
21
+ * {
22
+ * "kind": "action",
23
+ * "name": "seat 0 marks an empty cell",
24
+ * "config": {},
25
+ * "state": { ... }, // TS: applyAction input
26
+ * "obs": { ... }, // Dart-only; defaults to `state`
27
+ * "pending": [0],
28
+ * "playerIndex": 0,
29
+ * "participantCount": 2, // optional, default 2
30
+ * "rngSeed": "any string", // optional, default "twin-fixtures"
31
+ * "action": { ... },
32
+ * "expected": {
33
+ * "valid": true, // false ⇒ TS IllegalMoveError,
34
+ * // Dart isValidAction false
35
+ * "state": { ... }, // optional, TS envelope.state
36
+ * "pending": [1], // optional, TS envelope.pendingPlayers
37
+ * "outcome": [ ... ], // optional, TS envelope.outcome
38
+ * // (null asserts the game is ongoing)
39
+ * "observation": { ... } // optional, the actor's post-action
40
+ * // view: TS computeObservation slice
41
+ * // data; Dart previewAction (when the
42
+ * // game implements optimism)
43
+ * }
44
+ * },
45
+ * { "kind": "playerLimits", "name": "...", "config": {},
46
+ * "expected": { "minPlayers": 2, "maxPlayers": 2 } },
47
+ * { "kind": "ratingPool", "name": "...", "access": "public",
48
+ * "minPlayers": 2, "maxPlayers": 2, "config": {}, "expected": "blitz" },
49
+ * { "kind": "botSeatable", "name": "...", "gameConfig": {},
50
+ * "botConfig": {}, "expected": false }
51
+ * ]
52
+ * }
53
+ * ```
54
+ *
55
+ * The `state`/`obs` split exists for hidden-info games (a seat's observation
56
+ * is not the state); perfect-info games omit `obs`. `expected.observation` is
57
+ * the shared behavioral anchor: the TS side must project the post-action
58
+ * state to it, and a Dart `previewAction` that returns non-null must predict
59
+ * it, so the two sides are compared through one recorded value.
60
+ *
61
+ * Wire it up in a game-owned test file running under plain-Node vitest:
62
+ *
63
+ * ```ts
64
+ * import { twinFixtureTests } from "@eigeninteractive/testkit";
65
+ * import gameModule from "../src/module/index.js";
66
+ *
67
+ * twinFixtureTests(gameModule, new URL("../src/module/fixtures/", import.meta.url));
68
+ * ```
69
+ */
70
+ import { type GameAccess, type GameModule, type GameRules, type Json, type JsonObject, type OutcomeEntry } from "@eigeninteractive/rules";
71
+ /** One fixture file: cases targeting one `schemaVersion` unit. */
72
+ export interface TwinFixtureFile {
73
+ schemaVersion: number;
74
+ cases: TwinFixtureCase[];
75
+ }
76
+ /** A game-action case: exercises schemas, `applyAction`, and (through
77
+ * `expected.observation`) `computeObservation` for the acting seat. */
78
+ export interface ActionCase {
79
+ kind: "action";
80
+ name: string;
81
+ config: JsonObject;
82
+ state: JsonObject;
83
+ /** Dart-side observation payload; unused here (defaults to `state`). */
84
+ obs?: JsonObject;
85
+ pending: number[];
86
+ playerIndex: number;
87
+ participantCount?: number;
88
+ rngSeed?: string;
89
+ action: JsonObject;
90
+ expected: {
91
+ valid: boolean;
92
+ state?: JsonObject;
93
+ pending?: number[];
94
+ outcome?: OutcomeEntry[] | null;
95
+ observation?: JsonObject;
96
+ };
97
+ }
98
+ /** A `ratingPool` predicate case. Omitted timing fields mean null. */
99
+ export interface RatingPoolCase {
100
+ kind: "ratingPool";
101
+ name: string;
102
+ access: GameAccess;
103
+ turnSeconds?: number | null;
104
+ budgetSeconds?: number | null;
105
+ incrementSeconds?: number | null;
106
+ minPlayers: number;
107
+ maxPlayers: number;
108
+ config: JsonObject;
109
+ expected: string | null;
110
+ }
111
+ /** A `playerLimits` case: the seats one config may be played with. */
112
+ export interface PlayerLimitsCase {
113
+ kind: "playerLimits";
114
+ name: string;
115
+ config: JsonObject;
116
+ expected: {
117
+ minPlayers: number;
118
+ maxPlayers: number;
119
+ };
120
+ }
121
+ /** A `botSeatable` predicate case. */
122
+ export interface BotSeatableCase {
123
+ kind: "botSeatable";
124
+ name: string;
125
+ gameConfig: JsonObject;
126
+ botConfig: JsonObject;
127
+ expected: boolean;
128
+ }
129
+ export type TwinFixtureCase = ActionCase | PlayerLimitsCase | RatingPoolCase | BotSeatableCase;
130
+ /** Validate one fixture file's parsed JSON, or throw naming the offending
131
+ * file, case, and field. Exported so a repo can lint its fixtures without
132
+ * running them. */
133
+ export declare function parseTwinFixtureFile(path: string, json: unknown): TwinFixtureFile;
134
+ /** Run one fixture case against a rules unit, returning failure descriptions
135
+ * (empty ⇒ the case passes). Pure; the file-reading test registrar is
136
+ * {@link twinFixtureTests}. */
137
+ export declare function evaluateTwinCase(rules: GameRules, kase: TwinFixtureCase): string[];
138
+ /** Register one vitest test per fixture case found under `fixturesRoot`
139
+ * (layout: `<root>/v<N>/*.json`). Call at the top level of a test module
140
+ * running in a Node environment. */
141
+ export declare function twinFixtureTests(gameModule: GameModule, fixturesRoot: string | URL): void;
142
+ /** Structural JSON equality. Object keys with `undefined` values count as
143
+ * absent (matching how schema libraries model optional fields); array order
144
+ * matters. */
145
+ export declare function deepEquals(a: Json | undefined, b: Json | undefined): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigeninteractive/testkit",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "license": "MIT",
5
5
  "description": "Test helpers for games built on the EigenInteractive engine: drive rules through the real kernel and run shared twin fixtures, with no Worker, database or network.",
6
6
  "main": "./dist/index.js",
@@ -35,17 +35,17 @@
35
35
  "type": "module",
36
36
  "dependencies": {
37
37
  "rand-seed": "^3.0.0",
38
- "tsx": "^4.23.1",
39
- "@eigeninteractive/kernel": "0.5.0"
38
+ "tsx": "^4.23.12",
39
+ "@eigeninteractive/kernel": "0.5.2"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "^26.1.1",
42
+ "@types/node": "^26.2.0",
43
43
  "vitest": "^4.1.10",
44
- "@eigeninteractive/rules": "0.5.0"
44
+ "@eigeninteractive/rules": "0.5.2"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "vitest": "^4.1.0",
48
- "@eigeninteractive/rules": "^0.5.0"
48
+ "@eigeninteractive/rules": "^0.5.2"
49
49
  },
50
50
  "homepage": "https://eigeninteractive.com",
51
51
  "repository": {
@@ -60,7 +60,7 @@
60
60
  "access": "public"
61
61
  },
62
62
  "scripts": {
63
- "build": "tsup",
63
+ "build": "tsup && tsc -p tsconfig.build.json",
64
64
  "dev": "tsup --watch",
65
65
  "test": "vitest run --passWithNoTests",
66
66
  "typecheck": "tsc --noEmit -p tsconfig.json"