@irtio/testing 3.0.0 → 3.1.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/{chunk-3W5KPX3I.js → chunk-47O3HZBE.js} +122 -4
- package/dist/index.d.ts +30 -1
- package/dist/index.js +1 -1
- package/dist/matchers.js +1 -1
- package/package.json +7 -6
|
@@ -86,6 +86,7 @@ function clockScheduler(clock) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// src/harness.ts
|
|
89
|
+
import { spawnBots } from "@irtio/bots";
|
|
89
90
|
import { INTERNAL_SESSION, joinRoom } from "@irtio/client";
|
|
90
91
|
import {
|
|
91
92
|
ErrorCode,
|
|
@@ -259,6 +260,7 @@ function frameNameOf(frame) {
|
|
|
259
260
|
async function microtasks(turns = MICROTASK_TURNS) {
|
|
260
261
|
for (let i = 0; i < turns; i++) await Promise.resolve();
|
|
261
262
|
}
|
|
263
|
+
var NPC_TRACE_LIMIT = 16;
|
|
262
264
|
function detailOf(detail) {
|
|
263
265
|
return detail ? { detail } : {};
|
|
264
266
|
}
|
|
@@ -282,6 +284,8 @@ var TestHarness = class {
|
|
|
282
284
|
/** Client ids a frame was dropped for because nobody knows them, each warned about once. */
|
|
283
285
|
unknownSendWarned = /* @__PURE__ */ new Set();
|
|
284
286
|
clientList = [];
|
|
287
|
+
/** D44: live NPC sessions by client id. */
|
|
288
|
+
npcSessions = /* @__PURE__ */ new Map();
|
|
285
289
|
roleById = /* @__PURE__ */ new Map();
|
|
286
290
|
roleOverrides = /* @__PURE__ */ new Map();
|
|
287
291
|
traceLog = [];
|
|
@@ -340,6 +344,8 @@ var TestHarness = class {
|
|
|
340
344
|
}
|
|
341
345
|
this.holdOrDrop(clientId, frame);
|
|
342
346
|
};
|
|
347
|
+
this.host.onSpawnNpc = (clientId, config) => this.spawnNpc(clientId, config);
|
|
348
|
+
this.host.onDespawnNpc = (clientId) => this.despawnNpc(clientId);
|
|
343
349
|
this.coreRef = new RoomCore(this.definition, this.host, {
|
|
344
350
|
roomId: this.roomId,
|
|
345
351
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
@@ -437,13 +443,20 @@ var TestHarness = class {
|
|
|
437
443
|
// -------------------------------------------------------------------------
|
|
438
444
|
// The link
|
|
439
445
|
// -------------------------------------------------------------------------
|
|
440
|
-
/**
|
|
441
|
-
|
|
446
|
+
/**
|
|
447
|
+
* A `Transport` bound to one `t.join()`; reconnects call `connect` again with the same spec.
|
|
448
|
+
*
|
|
449
|
+
* `identity` is how an NPC gets in: the runtime mints its client id (`npc-<room>-<n>`) before the
|
|
450
|
+
* host is asked to open the session, and every reconnect this transport makes has to come back as
|
|
451
|
+
* that same id with the same `npc` flag, exactly as the supervisor's loopback does.
|
|
452
|
+
*/
|
|
453
|
+
transportFor(spec, identity) {
|
|
442
454
|
return {
|
|
443
455
|
connect: () => {
|
|
444
456
|
const link = {
|
|
445
457
|
socket: void 0,
|
|
446
|
-
clientId: `c${this.nextClientId++}`,
|
|
458
|
+
clientId: identity?.clientId ?? `c${this.nextClientId++}`,
|
|
459
|
+
npc: identity?.npc ?? false,
|
|
447
460
|
role: "",
|
|
448
461
|
joined: false,
|
|
449
462
|
connected: true,
|
|
@@ -463,6 +476,105 @@ var TestHarness = class {
|
|
|
463
476
|
}
|
|
464
477
|
};
|
|
465
478
|
}
|
|
479
|
+
// -------------------------------------------------------------------------
|
|
480
|
+
// D44: NPCs
|
|
481
|
+
// -------------------------------------------------------------------------
|
|
482
|
+
/**
|
|
483
|
+
* `room.spawnNPC`, in process. The runtime has already validated the config and minted the id;
|
|
484
|
+
* what is left is the supervisor's half — find the script in the definition's `npcs` map and put
|
|
485
|
+
* a real client behind it.
|
|
486
|
+
*
|
|
487
|
+
* Everything here rides `this.clock`: the bot joins through `transportFor`, so its frames are the
|
|
488
|
+
* same fake-clock timers a player's are, and `scheduler` makes `npc.wait` / `npc.until` fake time
|
|
489
|
+
* too. So an NPC spawned inside a tick becomes visible on the next `t.tick()` / `t.run()` /
|
|
490
|
+
* `t.settle`, and never makes progress behind the test's back.
|
|
491
|
+
*
|
|
492
|
+
* Throwing is deliberate: the caller is `room.spawnNPC` inside a handler, which the runtime runs
|
|
493
|
+
* under `tryRun`, so the sentence lands in `t.handlerErrors` and (by default) fails the test at
|
|
494
|
+
* the clock call that ran the handler. Recording it and carrying on is what this used to do, and
|
|
495
|
+
* it is how "my NPC never showed up" became a silent test.
|
|
496
|
+
*/
|
|
497
|
+
spawnNpc(clientId, config) {
|
|
498
|
+
if (this.stopped) return;
|
|
499
|
+
if (this.npcSessions.has(clientId)) {
|
|
500
|
+
this.host.log("warn", [`irtio: npc ${clientId} is already running; spawn ignored`]);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const scripts = this.definition.config.npcs;
|
|
504
|
+
const script = scripts?.[config.brain.script];
|
|
505
|
+
if (!script) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`testRoom: no npc script named ${JSON.stringify(config.brain.script)} in this room's npcs map`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const name = config.name ?? config.brain.script;
|
|
511
|
+
const entry = {
|
|
512
|
+
clientId,
|
|
513
|
+
script: config.brain.script,
|
|
514
|
+
runner: void 0,
|
|
515
|
+
stopped: false
|
|
516
|
+
};
|
|
517
|
+
this.npcSessions.set(clientId, entry);
|
|
518
|
+
this.activity++;
|
|
519
|
+
void spawnBots(1, {
|
|
520
|
+
schema: this.definition.schema,
|
|
521
|
+
room: this.roomId,
|
|
522
|
+
url: TEST_URL,
|
|
523
|
+
key: TEST_KEY,
|
|
524
|
+
name,
|
|
525
|
+
...config.role !== void 0 ? { role: config.role } : {},
|
|
526
|
+
...config.seed !== void 0 ? { seed: config.seed } : {},
|
|
527
|
+
traceLimit: NPC_TRACE_LIMIT,
|
|
528
|
+
// Both of these are real timers in `@irtio/bots`, and a fake-clock harness must not own one:
|
|
529
|
+
// the join is bounded by the test's own clock calls, and "every bot disconnected" is what
|
|
530
|
+
// `t.stop()` deliberately does.
|
|
531
|
+
joinTimeoutMs: 0,
|
|
532
|
+
roomGoneGraceMs: 0,
|
|
533
|
+
transport: this.transportFor({}, { clientId, npc: true }),
|
|
534
|
+
scheduler: this.scheduler,
|
|
535
|
+
script: (bot) => script(bot)
|
|
536
|
+
}).then(
|
|
537
|
+
(runner) => {
|
|
538
|
+
this.activity++;
|
|
539
|
+
entry.runner = runner;
|
|
540
|
+
if (entry.stopped) void this.stopNpc(entry);
|
|
541
|
+
else void runner.done().catch((err) => this.noteNpcError(entry, err));
|
|
542
|
+
},
|
|
543
|
+
(err) => {
|
|
544
|
+
this.npcSessions.delete(clientId);
|
|
545
|
+
this.noteNpcError(entry, err);
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
despawnNpc(clientId) {
|
|
550
|
+
const entry = this.npcSessions.get(clientId);
|
|
551
|
+
if (!entry) return;
|
|
552
|
+
this.npcSessions.delete(clientId);
|
|
553
|
+
entry.stopped = true;
|
|
554
|
+
if (entry.runner) void this.stopNpc(entry);
|
|
555
|
+
}
|
|
556
|
+
/** Stops one NPC's runner: its script winds down, then it leaves the room like any client. */
|
|
557
|
+
async stopNpc(entry) {
|
|
558
|
+
const runner = entry.runner;
|
|
559
|
+
entry.runner = void 0;
|
|
560
|
+
if (!runner) return;
|
|
561
|
+
this.activity++;
|
|
562
|
+
try {
|
|
563
|
+
await runner.stop();
|
|
564
|
+
} catch (err) {
|
|
565
|
+
this.noteNpcError(entry, err);
|
|
566
|
+
}
|
|
567
|
+
this.activity++;
|
|
568
|
+
}
|
|
569
|
+
/** An NPC failure, filed where a handler throw is filed — see `reportHandlerErrors`. */
|
|
570
|
+
noteNpcError(entry, err) {
|
|
571
|
+
this.handlerErrorLog.push({
|
|
572
|
+
handler: `npcs.${entry.script} (${entry.clientId})`,
|
|
573
|
+
message: err instanceof Error ? err.message : String(err),
|
|
574
|
+
tick: this.coreRef.tick
|
|
575
|
+
});
|
|
576
|
+
this.activity++;
|
|
577
|
+
}
|
|
466
578
|
/**
|
|
467
579
|
* Bug #49: a frame for a client with no link. Held when the core has the client and counts it
|
|
468
580
|
* connected, which is exactly the window between `RoomCore.join` running `onJoin` and
|
|
@@ -635,7 +747,8 @@ var TestHarness = class {
|
|
|
635
747
|
result = this.coreRef.join(link.clientId, {
|
|
636
748
|
...role !== void 0 ? { role } : {},
|
|
637
749
|
...name !== void 0 ? { name } : {},
|
|
638
|
-
...reconnecting ? { reconnecting: true } : {}
|
|
750
|
+
...reconnecting ? { reconnecting: true } : {},
|
|
751
|
+
...link.npc ? { npc: true } : {}
|
|
639
752
|
});
|
|
640
753
|
} catch (err) {
|
|
641
754
|
failure = err;
|
|
@@ -1218,6 +1331,11 @@ var TestHarness = class {
|
|
|
1218
1331
|
}
|
|
1219
1332
|
stop() {
|
|
1220
1333
|
this.stopped = true;
|
|
1334
|
+
for (const entry of [...this.npcSessions.values()]) {
|
|
1335
|
+
this.npcSessions.delete(entry.clientId);
|
|
1336
|
+
entry.stopped = true;
|
|
1337
|
+
void this.stopNpc(entry);
|
|
1338
|
+
}
|
|
1221
1339
|
for (const client of this.clientList) client.leave();
|
|
1222
1340
|
for (const link of this.links) link.socket.hangUp();
|
|
1223
1341
|
this.coreRef.stop();
|
package/dist/index.d.ts
CHANGED
|
@@ -407,6 +407,8 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
|
|
|
407
407
|
/** Client ids a frame was dropped for because nobody knows them, each warned about once. */
|
|
408
408
|
private readonly unknownSendWarned;
|
|
409
409
|
private readonly clientList;
|
|
410
|
+
/** D44: live NPC sessions by client id. */
|
|
411
|
+
private readonly npcSessions;
|
|
410
412
|
private readonly roleById;
|
|
411
413
|
private readonly roleOverrides;
|
|
412
414
|
private readonly traceLog;
|
|
@@ -468,8 +470,35 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
|
|
|
468
470
|
/** M7 pf4: the RPC trace, or `undefined` unless `traceRpc: true` armed one. */
|
|
469
471
|
get rpcTrace(): RpcTraceDump | undefined;
|
|
470
472
|
private enterCore;
|
|
471
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* A `Transport` bound to one `t.join()`; reconnects call `connect` again with the same spec.
|
|
475
|
+
*
|
|
476
|
+
* `identity` is how an NPC gets in: the runtime mints its client id (`npc-<room>-<n>`) before the
|
|
477
|
+
* host is asked to open the session, and every reconnect this transport makes has to come back as
|
|
478
|
+
* that same id with the same `npc` flag, exactly as the supervisor's loopback does.
|
|
479
|
+
*/
|
|
472
480
|
private transportFor;
|
|
481
|
+
/**
|
|
482
|
+
* `room.spawnNPC`, in process. The runtime has already validated the config and minted the id;
|
|
483
|
+
* what is left is the supervisor's half — find the script in the definition's `npcs` map and put
|
|
484
|
+
* a real client behind it.
|
|
485
|
+
*
|
|
486
|
+
* Everything here rides `this.clock`: the bot joins through `transportFor`, so its frames are the
|
|
487
|
+
* same fake-clock timers a player's are, and `scheduler` makes `npc.wait` / `npc.until` fake time
|
|
488
|
+
* too. So an NPC spawned inside a tick becomes visible on the next `t.tick()` / `t.run()` /
|
|
489
|
+
* `t.settle`, and never makes progress behind the test's back.
|
|
490
|
+
*
|
|
491
|
+
* Throwing is deliberate: the caller is `room.spawnNPC` inside a handler, which the runtime runs
|
|
492
|
+
* under `tryRun`, so the sentence lands in `t.handlerErrors` and (by default) fails the test at
|
|
493
|
+
* the clock call that ran the handler. Recording it and carrying on is what this used to do, and
|
|
494
|
+
* it is how "my NPC never showed up" became a silent test.
|
|
495
|
+
*/
|
|
496
|
+
private spawnNpc;
|
|
497
|
+
private despawnNpc;
|
|
498
|
+
/** Stops one NPC's runner: its script winds down, then it leaves the room like any client. */
|
|
499
|
+
private stopNpc;
|
|
500
|
+
/** An NPC failure, filed where a handler throw is filed — see `reportHandlerErrors`. */
|
|
501
|
+
private noteNpcError;
|
|
473
502
|
/**
|
|
474
503
|
* Bug #49: a frame for a client with no link. Held when the core has the client and counts it
|
|
475
504
|
* connected, which is exactly the window between `RoomCore.join` running `onJoin` and
|
package/dist/index.js
CHANGED
package/dist/matchers.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/testing",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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
|
"repository": {
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@irtio/
|
|
32
|
-
"@irtio/
|
|
33
|
-
"@irtio/
|
|
34
|
-
"@irtio/
|
|
35
|
-
"@irtio/
|
|
31
|
+
"@irtio/bots": "3.1.0",
|
|
32
|
+
"@irtio/client": "3.1.0",
|
|
33
|
+
"@irtio/protocol": "3.1.0",
|
|
34
|
+
"@irtio/runtime": "3.1.0",
|
|
35
|
+
"@irtio/schema": "3.1.0",
|
|
36
|
+
"@irtio/server": "3.1.0"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"vitest": ">=3"
|