npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@twin-digital/minecraft-test-lib

v0.4.0

Published

In-memory fakes of the @minecraft/server object model, for testing Minecraft Bedrock behavior packs.

Readme

@twin-digital/minecraft-test-lib

In-memory fakes of the @minecraft/server object model, for testing Minecraft Bedrock behavior packs. The fakes hold state and mutate it as their members are called, so a test asserts that health is now 20 rather than that setCurrentValue was called with 20.

@minecraft/server ships type declarations with no runtime JavaScript, so a pack author has no double to test against and hand-rolls one per test. Those doubles cannot express the conditions that break real packs — a component that is absent, a reference that went invalid in the middle of the event that fired — and a double that returns a plausible-looking payload lets a handler take the wrong branch while the test still passes.

Install

npm install --save-dev @twin-digital/minecraft-test-lib

ESM only, with type declarations, and no runtime dependencies.

Every value and behaviour here was derived from @minecraft/server 2.8.0, which the package exports as SERVER_VERSION. The pin is stated and nothing more: no @minecraft/server peer range is declared, and nothing at install, configuration or run time compares your engine pin against it. A pack on a different 2.x pin installs and runs; where the two versions differ, this library answers for 2.8.0.

It depends on no test framework at run time: the fakes are plain objects, and a caller who wants call recording wraps one with their own spy library. The one exception is the /vitest subpath below, which is runner tooling and reaches vi; nothing else in the package imports a runner, and vitest is declared as an optional peer so a consumer who never imports that subpath resolves the package without a warning.

There are two published entry points and no others: the root barrel, and @twin-digital/minecraft-test-lib/vitest.

Getting started

import { createServer } from '@twin-digital/minecraft-test-lib'
import { installMyPack } from '../src/main.js' // the pack under test, not this library

const server = createServer()
installMyPack(server) // the pack takes { world, system, … }

The second line is the pack's own entry point, whatever it is called; this library exports nothing that installs anything.

This is the injection path: a fake reaches the code under test as an object the test passes in, and nothing is installed anywhere. A suite written this way keeps working exactly as written.

A pack that reaches the engine through a direct import { world } from '@minecraft/server' instead is reached the other way — see the next section.

Testing a pack that imports @minecraft/server

Most packs do not take their engine handles as a parameter. They import them:

import { world, system } from '@minecraft/server'
world.afterEvents.entityHurt.subscribe(/* … */)

@minecraft/server ships no runtime JavaScript, so that import cannot resolve under a test runner at all — the module has no main, module or exports entry, and the suite fails to start before any of your code runs. This package ships the module the runner points at instead.

The install is one entry

// vitest.config.ts
import { minecraftTestLib } from '@twin-digital/minecraft-test-lib/vitest'

export default {
  plugins: [minecraftTestLib()],
}

That is the whole install. The plugin points the runner's resolver at the aliased surface this package ships, points it at stubs for the @minecraft/* script modules the fakes do not cover, and contributes a setup module that installs a fresh server before each test file evaluates. You write no setup file, and no alias of your own.

The default: static imports, one scenario per file

With the plugin in place, a test file holds static imports and nothing else:

import { addComponent, createEntity, currentServer, withVanillaDimensions } from '@twin-digital/minecraft-test-lib'
import { world } from '@minecraft/server'

import '../src/main.js' // the pack, imported for its side effects

it('reacts to a hurt entity', () => {
  const server = currentServer()
  withVanillaDimensions(server)
  const sheep = createEntity(server, { typeId: 'minecraft:sheep', dimension: world.getDimension('overworld') })
  addComponent(sheep, 'minecraft:health', 20)
  sheep.applyDamage(1)
  // assert on what the pack did
})

No install call, no reset prelude, no ordering you write. The setup module ran before this file evaluated, so the pack's module-scope subscribe and system.runInterval calls landed on the server currentServer() returns — the same object world and system are bound to.

Freshness is per file, which is the runner's own module-registry boundary. State carries between the tests within a file: a test that spawns an entity and advances twenty ticks hands the next test that entity and that clock. Write one scenario per file, or reach for loadPack.

The escape hatch: loadPack

import { advanceTicks } from '@twin-digital/minecraft-test-lib'
import { loadPack } from '@twin-digital/minecraft-test-lib/vitest'

it('starts from a world of its own', async () => {
  const server = await loadPack(() => import('../src/main.js'))
  advanceTicks(server, 20)
})

loadPack resets the module registry, imports this library fresh, installs a new server, and only then calls your importer — so the pack evaluates against a world no previous test touched — and hands back that server. Assert through the value it returns.

This is not an equal alternative to the default; it is for the cases that need a fresh evaluation: pack module-scope state a test mutates, load-time behaviour itself, scheduled-run accumulation, or a server that must differ before the pack evaluates (loadPack(importer, { server })).

The package ships no reset, public or internal. A fresh start comes from a fresh module-registry generation or from an explicit unset, never from swapping a live server out from under a pack that already registered against it.

Installing your own fakes

__useServer(server) points all three bindings at a server you built, and __useServer() returns them to the unset state. Reading through an unset binding throws ShimNotInstalledError rather than reading undefined.

Replacing a server a pack has already registered against throws ShimServerInUseError, naming the subscriber and scheduled-run counts it would have stranded: those registrations stay on the server the pack evaluated against, so the replacement would see none of the pack's behaviour. Unset first, or use loadPack.

What the aliased surface carries

Everything the pinned declarations declare as a value, and nothing else:

  • every enum, as a frozen object whose members are generated from the 2.8.0 declarations
  • the module-level numeric constants — TicksPerSecond, TicksPerDay, and their siblings
  • every declared class, and the three bindings world, system and EntityTypes

The classes the fakes implement are the fake classes, so instanceof answers by class identity: an entity this library built is an Entity the pack imported, and a fake carries the declared inheritance too — a player is an Entity, a health component is an EntityComponent. No brand, no Symbol.hasInstance, and nothing for a consumer to name.

A name the pinned declarations do not declare is not exported — no Proxy over unknown names, no auto-vivified stub, no fallback value. A pack importing a name 2.8.0 does not carry fails at the import.

The surface supplies values, classes and the three bindings. It models no behaviour of its own: every behaviour a test observes comes from the fakes.

The server

createServer() returns a fake server whose properties are named exactly as @minecraft/server exports them — world, system, and the eight type catalogs BiomeTypes, BlockStates, BlockTypes, DimensionTypes, EffectTypes, EnchantmentTypes, EntityTypes and ItemTypes — so it is assignable to a Pick<> of the module's namespace type and a pack written to receive its engine handles as a parameter can be handed the whole thing.

EntityTypes reads that server's own type catalog. The other seven are declared and every member on them throws NotImplementedError.

Entity types

A server's type catalog starts empty, and registerEntityType fills it. Both branches a lookup can take are arrangeable from the first line of a test — a type that resolves and a type that does not:

const server = createServer()
const guard = registerEntityType(server, 'mypack:guard')

server.EntityTypes.get('mypack:guard') === guard // the registered type
server.EntityTypes.get('mypack:absent') // undefined

withVanillaEntityTypes(server) registers the vanilla ids in one call. Lookup reproduces the engine's: a bare identifier resolves as minecraft:<id> and nothing else — no other namespace is searched, so a pack-defined type never answers to its bare name — the match is exact, so whitespace and case differences miss, and a miss reads undefined rather than throwing.

dimension.spawnEntity resolves through the catalog and throws InvalidArgumentError naming the identifier where nothing registers it, taking an EntityType wherever it takes an id. createEntity and createPlayer do not consult it: the engine declares no function at all for those, so they are the library's own and stay typeId-string-shaped.

The engine's catalog is read-only from script and carries whatever content the world installed; this one is written by the test. Two more differences are listed under Divergences below: the engine refuses every catalog read during early execution, which the fakes have no phase for, and the wording of the guard on a wrong-typed argument.

All state a server holds belongs to that server. Two createServer() calls in one process share nothing, so tests need no reset hook.

Every fake carries the full public shape of the type it stands in for and is assignable where the real declared type is expected, with no cast — the classes are generated from the pinned declarations, so implements checks completeness on every build. There is no Proxy and no runtime interception, which is what makes the fakes behave like ordinary objects: 'teleport' in entity is true, Object.keys reads the engine's two own properties, for-in walks its 62, and a spy library that wraps a method by assignment works.

Free functions

Everything the real API cannot express is a free function over the fakes rather than a member the engine does not have.

| function | what it does | | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | | createServer() | a new server: world, system, type catalogs | | createEntity(server, { typeId, id?, dimension?, location? }) | a fake entity registered with that world | | createPlayer(server, { typeId?, id?, name?, dimension?, location? }) | as above, a Player | | addComponent(entity, componentId, state?) | attach a component to a live entity | | removeComponent(entity, componentId) | detach one | | registerEffectBaseName(server, effectTypeId, baseName) | the base name for a custom effect type, or an override for a shipped one | | registerEntityType(server, id, localizationKey?) | put an entity type in that server's catalog | | invalidate(entity) | put the reference into the engine's invalid state | | emit(signal, payload) | deliver a payload to a signal's subscribers | | advanceTicks(server, count) | step the clock: decay effect durations, then run each tick's callbacks | | getOutput(target) | the messages and titles sent to a player or the world | | getTriggeredEvents(entity) | the triggerEvent calls made on an entity | | getHandlerErrors(server) | the errors thrown by subscribers and absorbed at dispatch | | __useServer(server?) | point the three module-scope bindings at a server, or unset them | | currentServer() | the server the module-scope bindings point at |

Presets

Populated starting points are invoked explicitly, never as constructor behaviour, and compose freely. Each supplies only values a source pins; neither invents per-type vanilla data.

  • withVanillaDimensions(server) adds the three vanilla dimensions. world.getDimension then resolves overworld, nether, the_end, their minecraft:-prefixed forms and the spaced alias "the end", each returning a dimension whose id is the prefixed form, with height ranges −64..320, 0..128 and 0..256 and localization keys dimension.dimensionName0/1/2.
  • withVanillaEntityTypes(server) registers the entity-type ids @minecraft/vanilla-data carries, in that source's order, so EntityTypes.get and dimension.spawnEntity answer for the vanilla types. An id you registered yourself is skipped rather than colliding with the duplicate refusal. The list is the source's, not a world's catalog read back: a world also carries whatever its content packs define.
  • withVanillaWorld(server) supplies those two and nothing else.
  • asSpawnedEntity(entity) supplies the spawn frame: nameTag the empty string, getRotation() {x: 0, y: 0} and getVelocity() {x: 0, y: 0, z: 0}. It supplies only what the caller left unset, so a nameTag you set survives it. It applies the same zeros to every type, including minecraft:xp_orb, which the engine spawns with a randomized rotation and velocity — a divergence, listed below.

Construction populates nothing

A new server has no dimensions, no players, no objectives and no dynamic properties, and a new entity carries no components and no field values beyond the ones you passed. That is deliberately unlike the engine, where a freshly spawned entity always arrives carrying at least one component.

Two kinds of nothing, told apart by the declaration's own type:

  • A value the engine could not lacknameTag, location, getRotation() — throws UnsetValueError naming the member when you never supplied it. A fake that invented one would let a handler branch on fiction.
  • An absence the engine can exhibit reads back as the engine reports it: getComponent for an unattached component, an unset dynamic property, and an unknown scoreboard objective or participant all return undefined. An empty collection is a real resting state.

Errors

None of the engine's error classes is importable at runtime, so the library declares its own.

Where the pinned declarations declare a class the library also hand-writes, the aliased surface exports the library's class — one class object per name — so a pack's catch (e) { e instanceof InvalidEntityError } catches what the fakes actually throw. Every other Error-ancestry class the declarations export is a real Error subclass setting its own name, with its declared readonly members left to whoever throws it. ArgumentOutOfBoundsError and InvalidArgumentError are @minecraft/common's and are not re-exported by @minecraft/server, so they stay names only this library exports.

| class | thrown when | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | InvalidEntityError | a member of an entity whose reference has gone invalid; carries the readonly id and type of that entity | | ArgumentOutOfBoundsError | a numeric argument falls outside the bounds the engine enforces — setCurrentValue, addEffect's amplifier and duration | | InvalidArgumentError | an argument's value is one the engine rejects outright — a bare id to triggerEvent | | NotImplementedError | a declared member this cycle does not model; names the member | | UnsetValueError | a modelled member reads a value the test never supplied; names the member | | ShimNotInstalledError | the module-scope world or system is reached before a test installed a server | | ShimServerInUseError | __useServer would replace a server a pack has already registered against; carries both counts |

Two guarded surfaces do not use InvalidEntityError, because the engine does not. On an invalid owner an attribute component's value getters throw a plain Error reading Failed to get property '<internal name>'. — the engine names its own field, current, value, effectiveMaxValue and effectiveMinValue — its three resets throw Failed to call function '<name>'., and an effect's amplifier, duration, typeId and displayName throw Failed to get property '<member>'.

What a read that finds nothing does

Five rules, in this order. A member matching an earlier rule never reaches a later one.

  1. Too few arguments throw TypeError first of all, ahead of the guard, on a valid and an invalidated reference alike: Incorrect number of arguments to function. Expected 2-3, received 0. Only the minimum is checked; extra arguments are ignored.
  2. The validity guard fires next. On an invalidated reference every guarded member throws InvalidEntityError — or the plain Error its owner's table gives — whatever the member would otherwise have done.
  3. An out-of-scope member throws NotImplementedError, however its declaration is typed.
  4. A modelled member reading an absence the engine can exhibit returns undefined.
  5. A modelled member reading a value the test never supplied throws UnsetValueError.

Invalidation

remove() invalidates as part of removing: it raises the entityRemove before-event, then detaches the entity and invalidates every reference to it as one act, then raises the after-event. invalidate(entity) reaches the state remove() cannot — the reference that goes stale without leaving the world — and may be called at any point, including on a reference a handler is holding mid-event.

On an invalidated entity exactly four members stay readable: id, isValid (false), typeId, and scoreboardIdentity (undefined). Every other member throws. The guard is on the call, not the read: reading a method off an invalidated entity returns a function, and a reference captured while the entity was still valid throws when it eventually runs.

Coverage

Every engine behaviour this library has ruled on is listed below as modelled (the fake reproduces the engine), not modelled (the members are declared and throw NotImplementedError, or the behaviour has no fake counterpart), or a divergence (the fake behaves, and differs from the engine on purpose). Each divergence row carries the difference itself, so this table is the one place to learn where a passing test would not have passed against the engine.

The table states what the design ruled on and nothing more: a behaviour outside it has not been considered, which is not the same as a promise about it.

Every row carries an id in its first column, and the id is the row's identity while its two description columns are not: pin the id, and expect the behaviour and library columns to be reworded without notice. An id names its row's subject rather than its verdict, so a row keeps its id when its coverage changes. An id is issued once — a subject that splits retires its id and both halves take new ones, and a removed subject's id is never reissued.

| id | engine behaviour | coverage | what the library does | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | dimension-registration-and-resolution | dimension registration and world.getDimension resolution | modelled | via withVanillaDimensions; ids, aliases, height ranges and localization keys as observed | | get-dimension-unknown-id | getDimension with an unknown id | modelled | plain Error, Dimension '<id>' is invalid. — including on a world where no preset was applied | | world-resting-state | the world's resting state — empty collections, no players, no objectives | modelled | | | fresh-entity-components | a freshly constructed entity's components | divergence | construction populates nothing; in the engine a fresh entity always arrives carrying at least one component | | xp-orb-spawn-frame | the spawn frame of minecraft:xp_orb | divergence | asSpawnedEntity applies zero rotation and velocity to every type; the engine spawns an xp_orb with a randomized rotation and a nonzero randomized velocity, drawn afresh per spawn | | per-type-vanilla-data | per-type vanilla data — a sheep's fourteen components, its 8/8/0/8 health | not modelled | no preset supplies it; a package built on this one may | | entity-id-assignment | entity id assignment | divergence | ids are decimal strings issued from 1 per server; the engine's are negative integers. Entity.id is documented opaque, so nothing may read the spelling either way | | entity-lookups | world.getEntity, getAllPlayers, getPlayers, dimension.getEntities, dimension.getPlayers | modelled | unfiltered, in creation order | | entity-query-options-filtering | EntityQueryOptions filtering, on the lookups and on entity.matches | divergence | six of the twenty-four fields filter — type, tags, name and their exclude counterparts; each of the other eighteen throws NotImplementedError naming itself, where the engine honours them all | | entity-tags | entity tags — addTag, removeTag, hasTag, getTags | modelled | a per-entity set, which the tags and excludeTags filters read | | positional-entity-lookups | the other entity lookups — getEntitiesAtBlockLocation, getEntitiesFromRay, getEntitiesFromViewDirection and the rest | not modelled | | | spawn-entity-placement | dimension.spawnEntity placement | divergence | the entity lands exactly where asked; the engine adjusts some placements — a boat by 0.2 on x and z | | post-spawn-motion | post-spawn motion | divergence | an entity never moves on its own; AI-driven mobs drift within a couple of dozen ticks | | entity-type-catalog | EntityTypes.get and EntityTypes.getAll | modelled | a bare identifier resolves as minecraft:<id> and nothing else — no other namespace is searched — the match is exact so whitespace and case differences miss, a miss reads undefined rather than throwing, getAll reports in registration order, and an entry is one object the catalog keeps rather than a value rebuilt per call | | entity-type-registration | how an entity type gets into the catalog | divergence | the engine's catalog is read-only from script and carries whatever content the world installed; here a server's catalog starts empty and registerEntityType(server, id, localizationKey?) fills it, refusing an id already registered rather than replacing the entry a test holds | | entity-type-catalog-early-execution | a catalog read during early execution | divergence | the engine refuses every read at a pack's module evaluation and inside a system.beforeEvents.startup handler, throwing ReferenceError: Native function [EntityTypes::get] cannot be used in early execution.; the fakes have no early phase, so a lookup answers whenever a test makes it | | entity-type-argument-guards | EntityTypes.get on a wrong-typed argument | divergence | the four wordings the engine splits into are reproduced for the six argument shapes measured, but every object outside those shapes takes the plain-object wording, which is this library's own extrapolation rather than an observation | | entity-type-shape | an EntityType's id and localizationKey | modelled | both are own value properties on the instance rather than getters, and the type carries no other member. An omitted key derives from the id as entity.<id>.name, with a leading minecraft: stripped and any other namespace kept | | spawn-entity-type-resolution | dimension.spawnEntity entity-type resolution | modelled | it resolves through the server's catalog, agreeing with EntityTypes.get on the same identifier, throws InvalidArgumentError naming the identifier where nothing registers it, and takes an EntityType wherever it takes an id | | create-entity-type-resolution | createEntity and createPlayer entity-type resolution | not modelled | the engine declares no function at all for these, so they are the library's own: each takes a typeId string, consults no catalog, and registers whatever type it is handed | | entity-remove-cascade | entity.remove() | modelled | raises the entityRemove before-event, then detaches from the registry and invalidates the reference as one act, then raises the after-event — the engine's own cascade, which raises no death event either | | trigger-event | entity.triggerEvent | divergence | validates the prefixed id and records the call, changing no state; in the engine the event reshapes the entity | | entity-kill-cascade | entity.kill() | modelled | the full cascade, on an entity with and without a health component | | corpse-invalidation-after-kill | invalidation of a mob's corpse after kill() | modelled | the corpse stays valid — inside the entityDie handler and after it — and turns invalid 21 ticks later, the constant the engine was measured at, so it goes stale when the test advances that far. Distinct from remove(), which invalidates at once | | kill-invalidation-without-health | invalidation after kill() on an entity with no health component | modelled | the reference goes invalid before entityDie is raised, as the engine's does within the call | | attribute-shaped-components | the seven attribute-shaped components | modelled | all four values, the bounds check, and the health-write cascade | | non-attribute-components | the other 61 entity components | not modelled | attachable, carrying typeId, isValid and entity; every other member throws | | runtime-component-mutation | runtime component attachment and detachment | not modelled | the engine reaches it through data-driven paths; a test uses the addComponent / removeComponent free functions | | namespace-prefix-tolerance | bare and prefixed id tolerance | modelled | per-surface, as observed — triggerEvent rejects the bare form and the others accept it | | set-current-value-bounds | setCurrentValue bounds check | modelled | including the message and both inclusive bounds | | apply-damage-cascade | applyDamage cascade, order and payloads | modelled | including the unclamped negative health an overkill leaves, and unrounded fractional amounts | | apply-damage-boolean | applyDamage's boolean | modelled | reports admission — damageable entity, positive amount — not whether damage landed, as observed | | apply-damage-cause-and-source | applyDamage cause defaults and the damagingEntity carry-through | modelled | | | killing-hit-boundary | the killing-hit boundary | modelled | reaching effectiveMin exactly is fatal on both the damage and the component-write path | | apply-damage-without-health | applyDamage on an entity with no health component | modelled | returns false, fires nothing, leaves the entity valid | | damage-invulnerability-window | the damage-invulnerability window | divergence | the fake has no i-frames, so consecutive applyDamage calls each take their full amount where the engine absorbs the second — a test driving repeated damage sees more health lost against the fake than the engine would take | | projectile-damage-adjustment | the engine's velocity-dependent projectile damage adjustment | divergence | the projectile options form applies the amount requested | | effect-add-and-replacement-rule | addEffect / getEffect / getEffects / removeEffect and the amplifier-first replacement rule | modelled | including the duration half of the rule, compared against the duration remaining as observed | | add-effect-argument-bounds | addEffect's argument bounds | modelled | amplifier 0…255, duration 1…20000000, ArgumentOutOfBoundsError outside either, nothing clamped, both message shapes reproduced | | add-effect-non-integer-arguments | addEffect's non-integer arguments | modelled | truncated toward zero, then bounds-checked — so duration 0.5 is refused | | add-effect-nan-and-infinity | addEffect on NaN or Infinity | divergence | the engine refuses these with a TypeError ahead of the bounds check; the fake does not reproduce that error's shape | | display-name-amplifier-mapping | the display name's amplifier mapping | modelled | bare base at amplifier 0, base plus the Roman numeral of amplifier + 1 at 1–5, bare base again from 6 to 255 — reproduced for all 37 vanilla types across the whole accepted amplifier range | | effect-duration-decay | effect duration decay | modelled | one per tick the test advances, the observed rate, applied ahead of that tick's callbacks; nothing decays unless the test advances | | effect-duration-expiry-boundary | what the engine does when a duration reaches zero | modelled | the effect is removed on the tick its decaying duration would reach 0, which is the boundary the engine was measured at: 0 is never readable, the last tick it reads is 1, getEffect and getEffects agree, and a handle captured beforehand answers as a removed effect's does. Nothing is dispatched on the way — 2.8.0 declares no effect-remove or effect-expire signal | | vanilla-effect-display-names | Effect.displayName for the 37 vanilla types | modelled | resolves with no test setup, from verbatim shipped base names and the computed numeral | | effect-display-name-locale | Effect.displayName in a locale other than the observed one | divergence | the shipped bases are the strings one server returned, and the API documents only a "player-friendly name" with no locale contract; until a second locale is observed the table is that locale's, and a test needing another registers its own bases | | custom-effect-display-name | Effect.displayName for a custom effect type | divergence | no base is shipped, so an unregistered custom type throws UnsetValueError where the engine would answer with whatever its own data holds | | signal-subscription | signal existence, subscribe / unsubscribe, reference dedupe and subscription order | modelled | | | filtered-subscription | a filtered subscription — an options argument to subscribe | modelled | on the five signals the fakes raise that declare an options type, every field that type carries filters as observed: entities by instance, entityTypes against the subject entity's prefixed typeId, allowedDamageCauses, and entityFilter through the entity-lookup matcher, intersecting where two are given. A bare entityTypes id matches nothing, as the engine matches nothing for one. A field a signal's options type does not carry, and any options argument on a signal the fakes never raise, throws NotImplementedError naming the field at the subscribe call | | after-event-dispatch-timing | after-event dispatch timing | divergence | synchronous, inside the causing call; the engine defers past that call's return to later in the same tick | | unraised-engine-signals | engine-raised signals outside the five after-events and three before-events the fakes raise | not modelled | no fake behaviour raises them; a test drives one with emit | | before-event-cancellation | before-event cancellation | modelled | on the two signals whose payload declares cancel | | cancelled-call-return-value | what a cancelled call returns | modelled | addEffect undefined, applyDamage true — the engine's own per-surface values, quirk included | | before-event-payload-writes | before-event mutable payload fields | divergence | writes to entityHurt.damage and effectAdd.duration are honoured, the duration write down its own validation path as the engine takes it — truncated toward zero, dropping the add entirely at or below zero, clamped to 20000000 above the maximum, and refused by the setter itself on NaN and Infinity. The other four declared mutable fields are writable but unread, which is the library's own: the fake raises no action that would consume them, so a write to weatherChange.duration changes nothing | | throwing-subscriber | a subscriber that throws | divergence | isolated as the engine isolates it, but the absorbed error is recorded for getHandlerErrors where the engine discards it | | tick-loop | the tick loop | divergence | nothing runs on its own; currentTick starts at 0 and moves only under advanceTicks