@irtio/testing 0.2.0 → 0.4.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.
@@ -189,6 +189,10 @@ var TestHarness = class {
189
189
  frameLeaks = [];
190
190
  spatialSeen = /* @__PURE__ */ new Map();
191
191
  rejectionLog = [];
192
+ /** Every handler throw the runtime caught, in order (bug 2). */
193
+ handlerErrorLog = [];
194
+ /** How many of them `reportHandlerErrors` has already raised. */
195
+ reportedErrors = 0;
192
196
  /** Reconnects completed per client id — bumped in `handleHello` on a resumed `WELCOME`. */
193
197
  reconnectCounts = /* @__PURE__ */ new Map();
194
198
  /** Prediction accounting per client id, fed by each room's `correct` events. */
@@ -204,12 +208,14 @@ var TestHarness = class {
204
208
  roomId;
205
209
  writeIntervalMs;
206
210
  latency;
211
+ failOnHandlerError;
207
212
  stopped = false;
208
213
  constructor(definition, options) {
209
214
  this.definition = configure(definition, options);
210
215
  this.roomId = options.roomId ?? "test-room";
211
216
  this.latency = options.latency;
212
217
  this.writeIntervalMs = options.writeIntervalMs;
218
+ this.failOnHandlerError = options.failOnHandlerError ?? true;
213
219
  this.rng = new Mulberry32(options.seed ?? 1);
214
220
  this.scheduler = clockScheduler(this.clock);
215
221
  this.host = new HarnessHost(this.clock);
@@ -223,6 +229,13 @@ var TestHarness = class {
223
229
  });
224
230
  this.host.core = this.coreRef;
225
231
  this.host.serializeForSave = () => this.coreRef.snapshot();
232
+ this.coreRef.onHandlerError = (handler, err) => {
233
+ this.handlerErrorLog.push({
234
+ handler,
235
+ message: err instanceof Error ? err.message : String(err),
236
+ tick: this.coreRef.tick
237
+ });
238
+ };
226
239
  this.coreRef.start();
227
240
  }
228
241
  // -------------------------------------------------------------------------
@@ -243,6 +256,9 @@ var TestHarness = class {
243
256
  get rejections() {
244
257
  return this.rejectionLog;
245
258
  }
259
+ get handlerErrors() {
260
+ return this.handlerErrorLog;
261
+ }
246
262
  get dropped() {
247
263
  return this.droppedFrames;
248
264
  }
@@ -610,10 +626,32 @@ var TestHarness = class {
610
626
  const before = this.activity;
611
627
  await microtasks();
612
628
  this.clock.advance(0);
613
- if (this.activity === before) return;
629
+ if (this.activity === before) return this.reportHandlerErrors();
614
630
  }
615
631
  throw new Error("testRoom: the frame pump did not settle");
616
632
  }
633
+ /**
634
+ * Bug 2: the runtime catches a throwing handler so the room stays up, which in a test means
635
+ * the failure arrives later, somewhere else, wearing a disguise (the one that found this was a
636
+ * frozen tick counter reported ten seconds on as a `t.until` timeout). Every clock method ends
637
+ * here, so the throw surfaces at the line that ran the tick that broke.
638
+ */
639
+ reportHandlerErrors() {
640
+ if (!this.failOnHandlerError) return;
641
+ const fresh = this.handlerErrorLog.slice(this.reportedErrors);
642
+ this.reportedErrors = this.handlerErrorLog.length;
643
+ if (fresh.length === 0) return;
644
+ const lines = fresh.map((e) => ` ${e.handler} (tick ${e.tick}): ${e.message}`);
645
+ const what = fresh.length === 1 ? "a handler" : `${fresh.length} handlers`;
646
+ throw new Error(
647
+ [
648
+ `testRoom: the room threw in ${what}`,
649
+ ...lines,
650
+ "The runtime caught it and the room kept running, which is what it does in production.",
651
+ "Pass `failOnHandlerError: false` to testRoom if a throwing handler is what this test is about."
652
+ ].join("\n")
653
+ );
654
+ }
617
655
  async tick(n = 1) {
618
656
  for (let i = 0; i < n; i++) {
619
657
  this.clock.advance(this.mode === "tick" ? this.intervalMs : 0);
package/dist/index.d.ts CHANGED
@@ -58,6 +58,17 @@ interface TestRoomOptions {
58
58
  readonly roomId?: string;
59
59
  /** The clients' owned-write flush window, in ms of fake time. Default 50 (the client's own). */
60
60
  readonly writeIntervalMs?: number;
61
+ /**
62
+ * Throw out of `t.tick` / `t.run` / `t.until` / `t.join` when a room handler throws. Default
63
+ * `true`.
64
+ *
65
+ * The runtime guards handlers on purpose: one bad `onJoin` must not take a live room down. In
66
+ * a test that same guarding hides the failure — the room limps on and the test fails ten
67
+ * seconds later on an unrelated `t.until` timeout, pointing at the wrong thing (bug 2). Set
68
+ * `false` for a test whose *subject* is a throwing handler; `t.handlerErrors` records them
69
+ * either way.
70
+ */
71
+ readonly failOnHandlerError?: boolean;
61
72
  }
62
73
  interface TestJoinSpec<Role extends string = string> {
63
74
  readonly role?: Role;
@@ -124,6 +135,13 @@ interface PredictionStats {
124
135
  /** Total pending local writes re-applied over corrections. */
125
136
  readonly replayed: number;
126
137
  }
138
+ /** One handler throw the runtime caught and the room survived. See `failOnHandlerError`. */
139
+ interface HandlerError {
140
+ /** What was running: `tick`, `physics`, `onJoin`, `room timer`, `alarms.<name>`, … */
141
+ readonly handler: string;
142
+ readonly message: string;
143
+ readonly tick: number;
144
+ }
127
145
  /** One `REPLY` that came back with `ok: false`, keyed by the rpc the client had called. */
128
146
  interface RejectedCall {
129
147
  readonly clientId: string;
@@ -141,6 +159,12 @@ interface TestRoom<S extends AnySchema> {
141
159
  readonly trace: readonly TraceEntry[];
142
160
  /** Error replies seen so far, for `toHaveRejected`. */
143
161
  readonly rejections: readonly RejectedCall[];
162
+ /**
163
+ * Handler throws the runtime caught, in order. Empty in a healthy room; with the default
164
+ * `failOnHandlerError` the first one has already failed the test by the time you could read
165
+ * this, so it is for tests that opted out.
166
+ */
167
+ readonly handlerErrors: readonly HandlerError[];
144
168
  /** Frames the `latency.loss` die dropped. */
145
169
  readonly dropped: number;
146
170
  /** Escape hatch: the real `RoomCore`. */
@@ -296,6 +320,10 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
296
320
  private readonly frameLeaks;
297
321
  private readonly spatialSeen;
298
322
  private readonly rejectionLog;
323
+ /** Every handler throw the runtime caught, in order (bug 2). */
324
+ private readonly handlerErrorLog;
325
+ /** How many of them `reportHandlerErrors` has already raised. */
326
+ private reportedErrors;
299
327
  /** Reconnects completed per client id — bumped in `handleHello` on a resumed `WELCOME`. */
300
328
  private readonly reconnectCounts;
301
329
  /** Prediction accounting per client id, fed by each room's `correct` events. */
@@ -311,6 +339,7 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
311
339
  private readonly roomId;
312
340
  private readonly writeIntervalMs;
313
341
  private readonly latency;
342
+ private readonly failOnHandlerError;
314
343
  private stopped;
315
344
  constructor(definition: RoomDefinition<S>, options: TestRoomOptions);
316
345
  get core(): RoomCore<S>;
@@ -318,6 +347,7 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
318
347
  get plain(): PlainState;
319
348
  get trace(): readonly TraceEntry[];
320
349
  get rejections(): readonly RejectedCall[];
350
+ get handlerErrors(): readonly HandlerError[];
321
351
  get dropped(): number;
322
352
  get now(): number;
323
353
  get tickCount(): number;
@@ -369,6 +399,13 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
369
399
  private record;
370
400
  /** Fires everything due, then lets the clients' promises run, until nothing new is in flight. */
371
401
  private settle;
402
+ /**
403
+ * Bug 2: the runtime catches a throwing handler so the room stays up, which in a test means
404
+ * the failure arrives later, somewhere else, wearing a disguise (the one that found this was a
405
+ * frozen tick counter reported ten seconds on as a `t.until` timeout). Every clock method ends
406
+ * here, so the throw surfaces at the line that ran the tick that broke.
407
+ */
408
+ private reportHandlerErrors;
372
409
  tick(n?: number): Promise<void>;
373
410
  run(ms: number): Promise<void>;
374
411
  /**
@@ -581,4 +618,4 @@ declare function testRoom<S extends AnySchema>(definition: RoomDefinition<S>, op
581
618
  /** Starts a schema-less relay room in-process and returns the test handle. See the module docblock. */
582
619
  declare function testRelay(options?: TestRelayOptions): Promise<TestRelay>;
583
620
 
584
- export { type LatencySpec, type PredictionStats, type RejectedCall, type TestClient, TestHarness, type TestJoinSpec, type TestRelay, type TestRelayClient, TestRelayHarness, type TestRelayJoinSpec, type TestRelayOptions, type TestRoom, type TestRoomOptions, type UntilOptions, clockScheduler, divergence, materialize, testRelay, testRoom };
621
+ export { type HandlerError, type LatencySpec, type PredictionStats, type RejectedCall, type TestClient, TestHarness, type TestJoinSpec, type TestRelay, type TestRelayClient, TestRelayHarness, type TestRelayJoinSpec, type TestRelayOptions, type TestRoom, type TestRoomOptions, type UntilOptions, clockScheduler, divergence, materialize, testRelay, testRoom };
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  toHaveRejected,
12
12
  toStayUnderBandwidth,
13
13
  toStayWithinPrediction
14
- } from "./chunk-6EHPS7A5.js";
14
+ } from "./chunk-Q44LJTMZ.js";
15
15
 
16
16
  // src/relay-harness.ts
17
17
  import { joinRelay } from "@irtio/client";
package/dist/matchers.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  toHaveRejected,
6
6
  toStayUnderBandwidth,
7
7
  toStayWithinPrediction
8
- } from "./chunk-6EHPS7A5.js";
8
+ } from "./chunk-Q44LJTMZ.js";
9
9
 
10
10
  // src/matchers.ts
11
11
  import { expect } from "vitest";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/testing",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "irtio's blessed test API: testRoom() in-process rooms with real client semantics, plus Vitest matchers",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -23,11 +23,11 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@irtio/client": "0.2.0",
27
- "@irtio/protocol": "0.2.0",
28
- "@irtio/runtime": "0.2.0",
29
- "@irtio/schema": "0.2.0",
30
- "@irtio/server": "0.2.0"
26
+ "@irtio/client": "0.4.0",
27
+ "@irtio/protocol": "0.4.0",
28
+ "@irtio/schema": "0.4.0",
29
+ "@irtio/server": "0.4.0",
30
+ "@irtio/runtime": "0.4.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "vitest": ">=3"