Skip to content

Testing SDK Reference

ts
import { createPluginTestHarness } from '@vibelands/plugin-sdk/testing';

Headless harness for plugin server entries. Mirrors the live host's quotas and permission gating with deterministic, in-memory infrastructure. Works with any test runner (the scaffold wires vitest).

createPluginTestHarness(hooks, options?)

ts
const harness = createPluginTestHarness(server, {
  pluginId: 'my-treasure',                 // default 'test-plugin'
  permissions: ['world.entities', 'storage', 'players.modify'],  // default: ALL
  config: { chestCount: 4 },               // raw config
  configSchema: manifest.configSchema,     // optional - resolves config like the host
  world: {
    worldId: 'test-world',
    terrainSeed: 4242,
    terrainSize: 600,
    seaLevel: 2,
    biome: 'grassland',
    timeOfDay: 0.5,
    weather: { type: 'clear', intensity: 0 },
    getGroundHeight: (x, z) => 6,          // your terrain stub
  },
  storage: { opened: { 'chest-1': 123 } }, // pre-seeded persistence ("previous session")
  verbose: false,                          // echo ctx.log to console
});

Pass your real manifest's permissions + configSchema to test exactly what ships.

Lifecycle drivers

MethodEffect
await harness.start()runs onWorldStart
harness.join(init)adds a player, runs onPlayerJoin; init: { sessionId, name?, userId?, level?, mode?, dead?, position? }
harness.leave(sessionId)removes the player, runs onPlayerLeave
harness.updatePlayer(sessionId, patch)mutates player state (position/dead/...) without firing hooks
await harness.message(type, data, sessionId)runs the onMessage[type] handler (throws if missing or payload > 8 KB)
harness.tick(dtSeconds = 0.1)runs onTick once
harness.advanceTimers(ms)advances the virtual clock; fires due ctx.schedule timers in order
await harness.dispose()runs onDispose, cancels timers

Inspection

MemberContains
harness.ctxthe full PluginServerContext - poke any API directly
harness.entitiesctx.entities (list/has/count - what clients would see)
harness.kv()parsed replicated KV blob
harness.storageDump()persistent storage as plain JSON
harness.sent / sentOfType(type, sessionId?)direct messages: { sessionId, type, data }
harness.broadcasts / broadcastsOfType(type)broadcasts: { type, data }
harness.rewards{ sessionId, moneyCents, xp, label } per grant
harness.logsevery ctx.log(...) call's args
harness.denialspermission ids that were denied

What's enforced like production

  • entity quota (384) and properties size (2 KB) - spawn/update return null/false
  • KV blob size (8 KB), storage value (32 KB) and total (2 MB) quotas
  • permission gating with identical deny semantics (logged no-op + recorded in denials)
  • message payload cap (8 KB) - the harness throws, since the host would silently drop
  • entity id suffix rules, config resolution, reward amount bounds

What's intentionally different

Live hostHarnessWhy
entity transforms batch at 10 Hz, KV defers over 2/sapplied immediatelydeterministic assertions
tick budget + circuit breakernonetests measure logic, not wall-clock
hook errors contained, plugin disablederrors rethrowtests should fail loudly
ctx.schedule uses real timersvirtual clock via advanceTimers()no sleeps in tests

One real-world note: Date.now() inside your plugin is not virtualized. Make wall-clock durations (respawn minutes etc.) configurable and test with small values.

Recipes

Restart persistence:

ts
const second = createPluginTestHarness(server, { storage: first.storageDump(), ... });

Permission regression:

ts
const harness = createPluginTestHarness(server, { permissions: [] });
await harness.start();
expect(harness.denials).toContain('world.entities');

Scheduled behavior:

ts
await harness.start();            // plugin set a 60 s announcement interval
harness.advanceTimers(60_000);
expect(harness.broadcastsOfType('announcement')).toHaveLength(1);

VibeLands Creator - Plugin SDK apiVersion 2