@eigeninteractive/testkit 0.5.1 → 0.6.0
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 -2
- package/dist/contract-cli.js +21 -22
- package/dist/contract-cli.js.map +1 -1
- package/dist/contract-command-aCqO5CyJ.js +542 -0
- package/dist/contract-command-aCqO5CyJ.js.map +1 -0
- package/dist/index.d.ts +159 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -55
- package/dist/index.js.map +1 -1
- package/dist/inspect-cli.d.ts +1 -16
- package/dist/inspect-cli.js +169 -153
- package/dist/inspect-cli.js.map +1 -1
- package/dist/local-state-CnO-Oi-R.js +467 -0
- package/dist/local-state-CnO-Oi-R.js.map +1 -0
- package/dist/local-state.d.ts +188 -224
- package/dist/local-state.d.ts.map +1 -0
- package/dist/local-state.js +2 -11
- package/package.json +10 -9
- package/src/contract-cli.ts +29 -0
- package/src/contract-command.ts +78 -0
- package/src/game-contract.ts +205 -0
- package/src/index.ts +53 -0
- package/src/inspect-cli.ts +231 -0
- package/{dist/kernel-scenarios.d.ts → src/kernel-scenarios.ts} +41 -5
- package/src/local-state.ts +638 -0
- package/src/twin-fixtures.ts +533 -0
- package/dist/chunk-BQIY636F.js +0 -483
- package/dist/chunk-BQIY636F.js.map +0 -1
- package/dist/chunk-YJE4AGHJ.js +0 -391
- package/dist/chunk-YJE4AGHJ.js.map +0 -1
- package/dist/contract-command.d.ts +0 -13
- package/dist/game-contract.d.ts +0 -58
- package/dist/local-state.js.map +0 -1
- package/dist/twin-fixtures.d.ts +0 -145
package/dist/local-state.d.ts
CHANGED
|
@@ -1,177 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read a Worker's LOCAL `.wrangler` state directly: the D1 index and the
|
|
3
|
-
* per-game Durable Object databases, opened as plain SQLite files.
|
|
4
|
-
*
|
|
5
|
-
* This is a development and debugging surface, never a runtime one. Nothing here
|
|
6
|
-
* runs on Workers: it is Node reading files that `wrangler dev` left on disk,
|
|
7
|
-
* which is why it lives in the testkit (a `devDependency`) behind its own
|
|
8
|
-
* subpath, and why it reads them through Node's built-in `node:sqlite` rather
|
|
9
|
-
* than a driver package. `better-sqlite3` would work and was tried first, but
|
|
10
|
-
* `drizzle-orm` declares it as an OPTIONAL PEER: installing it anywhere in the
|
|
11
|
-
* workspace re-resolves drizzle's peer set for every importer, which changed the
|
|
12
|
-
* types `packages/server` compiles against and broke its `dts` build. A
|
|
13
|
-
* debugging tool is not worth perturbing the engine's own dependency graph, and
|
|
14
|
-
* the built-in module costs nothing: no native build, no `allowBuilds` opt-in,
|
|
15
|
-
* and no dependency at all.
|
|
16
|
-
*
|
|
17
|
-
* Two stores, two different questions, and the reason a game-shaped reader
|
|
18
|
-
* exists at all:
|
|
19
|
-
*
|
|
20
|
-
* - **D1 is the index.** Discovery, history, ratings, identity. Its `games` row
|
|
21
|
-
* is a display read-model written by the DO as a fire-and-forget mirror.
|
|
22
|
-
* - **The game's DO is the session.** One SQLite database per game, holding the
|
|
23
|
-
* authoritative status, roster and the append-only transition log.
|
|
24
|
-
*
|
|
25
|
-
* A question like "why is this game not starting" therefore cannot be answered
|
|
26
|
-
* from either store alone, and answering it from a generic table browser means
|
|
27
|
-
* knowing which of the two to distrust. {@link LocalStore.game} joins them and
|
|
28
|
-
* reports the disagreement as a value, so a stale mirror reads as a stale
|
|
29
|
-
* mirror rather than as a mystery.
|
|
30
|
-
*
|
|
31
|
-
* Every open is read-only, so this is safe to run against a live `wrangler dev`.
|
|
32
|
-
*
|
|
33
|
-
* @module @eigeninteractive/testkit/local-state
|
|
34
|
-
*/
|
|
35
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
//#region src/local-state.d.ts
|
|
36
3
|
/** A local Durable Object database: one game's whole session. */
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
4
|
+
interface LocalDurableObject {
|
|
5
|
+
/** The Durable Object class, parsed from the directory Miniflare names
|
|
6
|
+
* `<worker>-<ClassName>`. */
|
|
7
|
+
className: string;
|
|
8
|
+
/** The hex `DurableObjectId`, which is the filename: `idFromName(name)`
|
|
9
|
+
* hashed. Not the game id, and not reversible without {@link name}. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** The name the object was addressed by, which for a game DO is its
|
|
12
|
+
* `gameId`. Null only if the object was created by unique id rather than by
|
|
13
|
+
* name, which the engine never does. */
|
|
14
|
+
name: string | null;
|
|
15
|
+
/** Absolute path to the SQLite file. */
|
|
16
|
+
file: string;
|
|
50
17
|
}
|
|
51
18
|
/** What a bound parameter may be: SQLite's own value domain, spelled out here
|
|
52
19
|
* rather than re-exported from `node:sqlite` so the public signature does not
|
|
53
20
|
* depend on a built-in module's type names. */
|
|
54
|
-
|
|
21
|
+
type LocalQueryParam = string | number | bigint | null | Uint8Array;
|
|
55
22
|
/** A row of Miniflare's alarm table: the DO's armed alarm, which for a game is
|
|
56
23
|
* its turn deadline plus grace. An absent row means no timer is armed, which is
|
|
57
24
|
* correct for an untimed game and a bug for a timed one mid-turn. */
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
25
|
+
interface LocalAlarm {
|
|
26
|
+
actorId: string;
|
|
27
|
+
actorName: string | null;
|
|
28
|
+
scheduledTime: number | null;
|
|
62
29
|
}
|
|
63
30
|
/** The engine's `games` row: the D1 index entry, a display mirror of the DO. */
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
31
|
+
interface LocalGameRow {
|
|
32
|
+
id: string;
|
|
33
|
+
createdBy: string | null;
|
|
34
|
+
status: string;
|
|
35
|
+
access: string;
|
|
36
|
+
schemaVersion: number;
|
|
37
|
+
config: unknown;
|
|
38
|
+
turnSeconds: number | null;
|
|
39
|
+
budgetSeconds: number | null;
|
|
40
|
+
incrementSeconds: number | null;
|
|
41
|
+
rated: boolean;
|
|
42
|
+
ratingPool: string | null;
|
|
43
|
+
minPlayers: number;
|
|
44
|
+
maxPlayers: number;
|
|
45
|
+
shortCode: string;
|
|
46
|
+
pendingPlayers: number[] | null;
|
|
47
|
+
turnDeadline: number | null;
|
|
48
|
+
outcomes: unknown;
|
|
49
|
+
finishId: string | null;
|
|
50
|
+
finishedAt: number | null;
|
|
51
|
+
archivedAt: number | null;
|
|
52
|
+
createdAt: number;
|
|
53
|
+
updatedAt: number;
|
|
87
54
|
}
|
|
88
55
|
/** One seat, from either store. `username` is filled in from D1's `users` and
|
|
89
56
|
* `bots` tables when the reader has an index to join against. */
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
57
|
+
interface LocalSeat {
|
|
58
|
+
playerIndex: number;
|
|
59
|
+
userId: string | null;
|
|
60
|
+
botId: string | null;
|
|
61
|
+
type: string;
|
|
62
|
+
username: string | null;
|
|
63
|
+
displayName: string | null;
|
|
97
64
|
}
|
|
98
65
|
/** The DO's single `meta` row: the authoritative session header. */
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
66
|
+
interface LocalGameMeta {
|
|
67
|
+
gameId: string;
|
|
68
|
+
status: string;
|
|
69
|
+
access: string;
|
|
70
|
+
schemaVersion: number;
|
|
71
|
+
config: unknown;
|
|
72
|
+
turnSeconds: number | null;
|
|
73
|
+
budgetSeconds: number | null;
|
|
74
|
+
incrementSeconds: number | null;
|
|
75
|
+
rated: boolean;
|
|
76
|
+
ratingPool: string | null;
|
|
77
|
+
minPlayers: number;
|
|
78
|
+
maxPlayers: number;
|
|
79
|
+
createdBy: string | null;
|
|
80
|
+
rngSeed: string | null;
|
|
114
81
|
}
|
|
115
82
|
/** One committed transition, decoded. `action` is null at v0 (the start). */
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
interface LocalTransition {
|
|
84
|
+
version: number;
|
|
85
|
+
state: unknown;
|
|
86
|
+
action: {
|
|
87
|
+
type: string;
|
|
88
|
+
kind: string;
|
|
89
|
+
playerIndex: number | null;
|
|
90
|
+
data: unknown;
|
|
91
|
+
} | null;
|
|
92
|
+
pending: number[];
|
|
93
|
+
deadline: number | null;
|
|
94
|
+
playerTimes: number[] | null;
|
|
95
|
+
/** Non-null only while this transition's pending turn consumes a budget
|
|
96
|
+
* bank. Per-action and hook-override deadlines do not set it. */
|
|
97
|
+
turnStartedAt: number | null;
|
|
98
|
+
/** Which seats hold a stored frame at this version. Empty after the finish
|
|
99
|
+
* compaction, which empties `frames` and leaves replay to re-projection. */
|
|
100
|
+
frameSeats: number[];
|
|
134
101
|
}
|
|
135
102
|
/** Why the game is where it is, and what would move it. Derived rather than
|
|
136
103
|
* stored: this is the sentence a developer actually wants, and every input to
|
|
137
104
|
* it is already on screen above it. */
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
105
|
+
interface LocalGameDiagnosis {
|
|
106
|
+
/** One line naming the next expected event, or the reason there is none. */
|
|
107
|
+
summary: string;
|
|
108
|
+
/** Disagreements between the D1 mirror and the DO's own truth. A non-empty
|
|
109
|
+
* list means a mirror write was lost, since the DO is authoritative. */
|
|
110
|
+
mirrorDrift: string[];
|
|
144
111
|
}
|
|
145
112
|
/** Everything both stores know about one game. */
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
finishId: string;
|
|
170
|
-
createdAt: number;
|
|
171
|
-
outcomes: unknown;
|
|
172
|
-
}[];
|
|
173
|
-
alarm: LocalAlarm | null;
|
|
174
|
-
diagnosis: LocalGameDiagnosis;
|
|
113
|
+
interface LocalGameView {
|
|
114
|
+
gameId: string;
|
|
115
|
+
/** The D1 index row, or null when D1 has no such game: either the id came
|
|
116
|
+
* from a DO name and the index was wiped, or the create write never landed. */
|
|
117
|
+
index: LocalGameRow | null;
|
|
118
|
+
/** D1's `participants`, which mirror the DO's roster for display. */
|
|
119
|
+
indexSeats: LocalSeat[];
|
|
120
|
+
/** The DO file, or null when no Durable Object exists for this game yet.
|
|
121
|
+
* That is the normal state of a created-but-never-touched lobby: the DO is
|
|
122
|
+
* created lazily by the first command or socket. */
|
|
123
|
+
durableObject: LocalDurableObject | null;
|
|
124
|
+
meta: LocalGameMeta | null;
|
|
125
|
+
roster: LocalSeat[];
|
|
126
|
+
transitions: LocalTransition[];
|
|
127
|
+
/** Unapplied finish rows: a surviving row means the D1 finish apply has not
|
|
128
|
+
* succeeded and the admin re-poke is the recovery. */
|
|
129
|
+
outbox: {
|
|
130
|
+
finishId: string;
|
|
131
|
+
createdAt: number;
|
|
132
|
+
outcomes: unknown;
|
|
133
|
+
}[];
|
|
134
|
+
alarm: LocalAlarm | null;
|
|
135
|
+
diagnosis: LocalGameDiagnosis;
|
|
175
136
|
}
|
|
176
137
|
/**
|
|
177
138
|
* Walk up from `from` to the nearest directory holding `.wrangler/state/v3`.
|
|
@@ -180,7 +141,7 @@ export interface LocalGameView {
|
|
|
180
141
|
* inside a game repository, including the app half of a two-package layout,
|
|
181
142
|
* where the state lives a directory or two above the shell's cwd.
|
|
182
143
|
*/
|
|
183
|
-
|
|
144
|
+
declare function findWranglerState(from?: string): string | null;
|
|
184
145
|
/**
|
|
185
146
|
* Open a SQLite file for reading only.
|
|
186
147
|
*
|
|
@@ -194,7 +155,7 @@ export declare function findWranglerState(from?: string): string | null;
|
|
|
194
155
|
* because that fallback would otherwise CREATE a database rather than report a
|
|
195
156
|
* missing one.
|
|
196
157
|
*/
|
|
197
|
-
|
|
158
|
+
declare function openSqlite(file: string): DatabaseSync;
|
|
198
159
|
/**
|
|
199
160
|
* A Worker's local state, with its databases opened lazily and held open.
|
|
200
161
|
*
|
|
@@ -202,76 +163,79 @@ export declare function openSqlite(file: string): DatabaseSync;
|
|
|
202
163
|
* {@link close} it. A single instance is the unit both the CLI and a test use,
|
|
203
164
|
* so neither has to know which file backs which resource.
|
|
204
165
|
*/
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
166
|
+
declare class LocalStore {
|
|
167
|
+
#private;
|
|
168
|
+
/** Absolute path to `.wrangler/state/v3`. */
|
|
169
|
+
readonly root: string;
|
|
170
|
+
private constructor();
|
|
171
|
+
/**
|
|
172
|
+
* Locate and open the state under `from`, or throw with the one thing the
|
|
173
|
+
* caller can act on: that no Worker has been run here yet.
|
|
174
|
+
*/
|
|
175
|
+
static open(from?: string): LocalStore;
|
|
176
|
+
/**
|
|
177
|
+
* The engine's D1 database, identified by its schema rather than its name.
|
|
178
|
+
*
|
|
179
|
+
* Miniflare names the file after a hash of the database, so there is no
|
|
180
|
+
* mapping back to the binding. Recognising the engine's own `games` table
|
|
181
|
+
* instead is both simpler and more honest about what the caller wants: the
|
|
182
|
+
* database that has games in it, whatever the binding was called.
|
|
183
|
+
*/
|
|
184
|
+
d1(): {
|
|
185
|
+
file: string;
|
|
186
|
+
db: DatabaseSync;
|
|
187
|
+
} | null;
|
|
188
|
+
/**
|
|
189
|
+
* Every local Durable Object database, newest-modified first.
|
|
190
|
+
*
|
|
191
|
+
* The directory is `<worker>-<ClassName>`, and a worker name may itself
|
|
192
|
+
* contain a dash, so the class is everything after the LAST one. Ordering by
|
|
193
|
+
* mtime puts the game you just touched at the top, which is nearly always the
|
|
194
|
+
* one being debugged.
|
|
195
|
+
*/
|
|
196
|
+
durableObjects(): LocalDurableObject[];
|
|
197
|
+
/** The armed alarm for a Durable Object, from its namespace's metadata
|
|
198
|
+
* database. Matched on either column because the id is what Miniflare keys
|
|
199
|
+
* by and the name is what a human recognises. */
|
|
200
|
+
alarm(target: LocalDurableObject): LocalAlarm | null;
|
|
201
|
+
/** The D1 index rows, newest-updated first. */
|
|
202
|
+
games(options?: {
|
|
203
|
+
status?: string;
|
|
204
|
+
limit?: number;
|
|
205
|
+
}): LocalGameRow[];
|
|
206
|
+
/**
|
|
207
|
+
* Resolve a game reference the way a developer will type it: a full id, a
|
|
208
|
+
* six-character short code, or an id prefix long enough to be unique.
|
|
209
|
+
*
|
|
210
|
+
* D1 is tried first because it can match a short code, then the DO names, so
|
|
211
|
+
* a game whose index row was wiped is still reachable by id.
|
|
212
|
+
*/
|
|
213
|
+
resolveGameId(ref: string): string | null;
|
|
214
|
+
/** The Durable Object holding a game's session, or null if none exists yet. */
|
|
215
|
+
gameDurableObject(gameId: string): LocalDurableObject | null;
|
|
216
|
+
/**
|
|
217
|
+
* Join both stores for one game: the index row, the session, the transition
|
|
218
|
+
* log, and the derived diagnosis.
|
|
219
|
+
*
|
|
220
|
+
* Returns null only when neither store has heard of the game.
|
|
221
|
+
*/
|
|
222
|
+
game(ref: string): LocalGameView | null;
|
|
223
|
+
/** Run a read-only statement against D1 or one game's Durable Object: the
|
|
224
|
+
* escape hatch for a question this reader does not model. */
|
|
225
|
+
query(sql: string, options?: {
|
|
226
|
+
game?: string;
|
|
227
|
+
params?: LocalQueryParam[];
|
|
228
|
+
}): Record<string, unknown>[];
|
|
229
|
+
/** Table names and row counts, for D1 or one game's Durable Object. */
|
|
230
|
+
tables(options?: {
|
|
231
|
+
game?: string;
|
|
232
|
+
}): {
|
|
233
|
+
name: string;
|
|
234
|
+
rows: number;
|
|
235
|
+
}[];
|
|
236
|
+
/** Release every open handle. */
|
|
237
|
+
close(): void;
|
|
277
238
|
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { LocalAlarm, LocalDurableObject, LocalGameDiagnosis, LocalGameMeta, LocalGameRow, LocalGameView, LocalQueryParam, LocalSeat, LocalStore, LocalTransition, findWranglerState, openSqlite };
|
|
241
|
+
//# sourceMappingURL=local-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-state.d.ts","names":[],"sources":["../src/local-state.ts"],"mappings":";;;UAqDiB;;;EAGf;;;EAGA;;;;EAIA;;EAEA;;;;;KAMU,oDAAoD;;;;UAK/C;EACf;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;UAKe;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;IAAU;IAAc;IAAc;IAA4B;;EAClE;EACA;EACA;;;EAGA;;;EAGA;;;;;UAMe;;EAEf;;;EAGA;;;UAIe;EACf;;;EAGA,OAAO;;EAEP,YAAY;;;;EAIZ,eAAe;EACf,MAAM;EACN,QAAQ;EACR,aAAa;;;EAGb;IAAU;IAAkB;IAAmB;;EAC/C,OAAO;EACP,WAAW;;;;;;;;;iBAUG,kBAAkB;;;;;;;;;;;;;;iBAwBlB,WAAW,eAAe;;;;;;;;cAiD7B;;;WAEF;UAKF;;;;;SAQA,KAAK,gBAAgB;;;;;;;;;EAwB5B;IAAQ;IAAc,IAAI;;;;;;;;;;EAqB1B,kBAAkB;;;;EAoBlB,MAAM,QAAQ,qBAAqB;;EAUnC,MAAM;IAAW;IAAiB;MAAwB;;;;;;;;EAgB1D,cAAc;;EAgBd,kBAAkB,iBAAiB;;;;;;;EAsCnC,KAAK,cAAc;;;EA2DnB,MAAM,aAAa;IAAW;IAAe,SAAS;MAA2B;;EASjF,OAAO;IAAW;;IAAyB;IAAc;;;EAczD"}
|
package/dist/local-state.js
CHANGED
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
findWranglerState,
|
|
4
|
-
openSqlite
|
|
5
|
-
} from "./chunk-YJE4AGHJ.js";
|
|
6
|
-
export {
|
|
7
|
-
LocalStore,
|
|
8
|
-
findWranglerState,
|
|
9
|
-
openSqlite
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=local-state.js.map
|
|
1
|
+
import { n as findWranglerState, r as openSqlite, t as LocalStore } from "./local-state-CnO-Oi-R.js";
|
|
2
|
+
export { LocalStore, findWranglerState, openSqlite };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eigeninteractive/testkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
23
|
"bin",
|
|
24
|
-
"dist"
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
25
26
|
],
|
|
26
27
|
"keywords": [
|
|
27
28
|
"eigen",
|
|
@@ -35,17 +36,17 @@
|
|
|
35
36
|
"type": "module",
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"rand-seed": "^3.0.0",
|
|
38
|
-
"tsx": "^4.23.
|
|
39
|
-
"@eigeninteractive/kernel": "0.
|
|
39
|
+
"tsx": "^4.23.12",
|
|
40
|
+
"@eigeninteractive/kernel": "0.6.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^26.
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
43
44
|
"vitest": "^4.1.10",
|
|
44
|
-
"@eigeninteractive/rules": "0.
|
|
45
|
+
"@eigeninteractive/rules": "0.6.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
48
|
"vitest": "^4.1.0",
|
|
48
|
-
"@eigeninteractive/rules": "^0.
|
|
49
|
+
"@eigeninteractive/rules": "^0.6.0"
|
|
49
50
|
},
|
|
50
51
|
"homepage": "https://eigeninteractive.com",
|
|
51
52
|
"repository": {
|
|
@@ -60,8 +61,8 @@
|
|
|
60
61
|
"access": "public"
|
|
61
62
|
},
|
|
62
63
|
"scripts": {
|
|
63
|
-
"build": "
|
|
64
|
-
"dev": "
|
|
64
|
+
"build": "tsdown",
|
|
65
|
+
"dev": "tsdown --watch",
|
|
65
66
|
"test": "vitest run --passWithNoTests",
|
|
66
67
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
67
68
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { checkConfiguredGameContract, emitConfiguredGameContract } from "./contract-command.js";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const { values } = parseArgs({
|
|
7
|
+
strict: true,
|
|
8
|
+
options: {
|
|
9
|
+
check: { type: "boolean" },
|
|
10
|
+
help: { type: "boolean", short: "h" },
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (values.help) {
|
|
14
|
+
console.log(`Usage: eigen-contract [options]
|
|
15
|
+
|
|
16
|
+
Emit game-contract.json from the configured game module and fixtures.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--check Fail when the committed contract is stale
|
|
20
|
+
-h, --help Show this help`);
|
|
21
|
+
} else {
|
|
22
|
+
if (values.check) await checkConfiguredGameContract();
|
|
23
|
+
else await emitConfiguredGameContract();
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
console.error(`eigen-contract: ${message}`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { GameModule } from "@eigeninteractive/rules";
|
|
5
|
+
import { tsImport } from "tsx/esm/api";
|
|
6
|
+
import { checkGameContract, emitGameContract } from "./game-contract.js";
|
|
7
|
+
|
|
8
|
+
interface EigenPackageConfig {
|
|
9
|
+
game?: unknown;
|
|
10
|
+
module?: unknown;
|
|
11
|
+
fixtures?: unknown;
|
|
12
|
+
contract?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PackageManifest {
|
|
16
|
+
eigen?: EigenPackageConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function configuredString(value: unknown, fallback: string, field: string): string {
|
|
20
|
+
if (value === undefined) return fallback;
|
|
21
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
22
|
+
throw new Error(`package.json eigen.${field} must be a non-empty string`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isGameModule = (value: unknown): value is GameModule => value !== null && typeof value === "object" && "versions" in value && value.versions !== null && typeof value.versions === "object";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit a contract from the conventional game package layout.
|
|
31
|
+
*
|
|
32
|
+
* The package default-exports its GameModule from `src/module/index.ts` and
|
|
33
|
+
* declares `{ "eigen": { "game": "…" } }` in package.json. Paths can be
|
|
34
|
+
* overridden under the same `eigen` object.
|
|
35
|
+
*/
|
|
36
|
+
async function configuredGameContractOptions(root: string) {
|
|
37
|
+
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as PackageManifest;
|
|
38
|
+
const config = manifest.eigen ?? {};
|
|
39
|
+
const game = configuredString(config.game, "", "game");
|
|
40
|
+
if (game.length === 0) {
|
|
41
|
+
throw new Error('package.json must declare an EigenInteractive game name, e.g. "eigen": { "game": "Chess" }');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const modulePath = configuredString(config.module, "src/module/index.ts", "module");
|
|
45
|
+
const fixturesPath = configuredString(config.fixtures, "src/module/fixtures", "fixtures");
|
|
46
|
+
const contractPath = configuredString(config.contract, "game-contract.json", "contract");
|
|
47
|
+
const imported = (await tsImport(pathToFileURL(resolve(root, modulePath)).href, import.meta.url)) as { default?: unknown };
|
|
48
|
+
|
|
49
|
+
if (!isGameModule(imported.default)) {
|
|
50
|
+
throw new Error(`${modulePath} must default-export an EigenInteractive GameModule`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
game,
|
|
55
|
+
gameModule: imported.default,
|
|
56
|
+
fixturesRoot: resolve(root, fixturesPath),
|
|
57
|
+
output: resolve(root, contractPath),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Emits `game-contract.json` from an EigenInteractive package's conventional layout.
|
|
63
|
+
*
|
|
64
|
+
* This is the programmatic form of the `eigen-contract` executable. Most
|
|
65
|
+
* games should invoke the executable through their package script.
|
|
66
|
+
*/
|
|
67
|
+
export async function emitConfiguredGameContract(root = process.cwd()): Promise<void> {
|
|
68
|
+
emitGameContract(await configuredGameContractOptions(root));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fails when the conventionally configured contract is absent or stale.
|
|
73
|
+
*
|
|
74
|
+
* Use this in CI through `eigen-contract --check`.
|
|
75
|
+
*/
|
|
76
|
+
export async function checkConfiguredGameContract(root = process.cwd()): Promise<void> {
|
|
77
|
+
checkGameContract(await configuredGameContractOptions(root));
|
|
78
|
+
}
|