@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.
- package/dist/contract-cli.d.ts +1 -0
- package/dist/contract-command.d.ts +13 -0
- package/dist/game-contract.d.ts +58 -0
- package/dist/index.d.ts +8 -246
- package/dist/inspect-cli.d.ts +15 -0
- package/dist/kernel-scenarios.d.ts +27 -0
- package/dist/local-state.d.ts +13 -17
- package/dist/twin-fixtures.d.ts +145 -0
- package/package.json +7 -7
package/dist/contract-cli.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
9
|
-
* games should invoke the executable through their package script.
|
|
6
|
+
* @module @eigeninteractive/testkit
|
|
10
7
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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";
|
package/dist/inspect-cli.d.ts
CHANGED
|
@@ -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;
|
package/dist/local-state.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
39
|
-
"@eigeninteractive/kernel": "0.5.
|
|
38
|
+
"tsx": "^4.23.12",
|
|
39
|
+
"@eigeninteractive/kernel": "0.5.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^26.
|
|
42
|
+
"@types/node": "^26.2.0",
|
|
43
43
|
"vitest": "^4.1.10",
|
|
44
|
-
"@eigeninteractive/rules": "0.5.
|
|
44
|
+
"@eigeninteractive/rules": "0.5.2"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"vitest": "^4.1.0",
|
|
48
|
-
"@eigeninteractive/rules": "^0.5.
|
|
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"
|