Skip to main content
Test against a domain that has something in it. On an empty server almost nothing you declare is exercised, and a run where nothing failed is not a run where anything passed.

Run the contract

One runner, for every adapter, built entirely on the public contract. You write no test harness and ship no test hooks. It works out which checks apply from what you declare, and it ships in the build customers install.

Two modes, and what each one touches

Read-only is no more invasive than Blueprints’ own use of your adapter. It calls the functions Blueprints already calls to capture a Version or preview a migration, which are the functions the contract forbids from writing. Full writes. No check ever targets an object that was already on the server: every destructive check works on a fixture the runner built seconds earlier through your own Materialize, and the run ends by releasing it. But what your adapter does to reach that fixture is real. An adapter backed by a database writes to it, and one whose storage is not object-scoped may touch neighbouring records to act on one: Vetra’s own DarkRP adapter releases a spawn by rewriting that job’s spawn set, ids preserved.
Run full on a development server. That is why it is refused to players, admins included.

Reading the report

NOT RUN is never a pass. Every skipped check carries its reason, and a read-only run says so in its verdict. A full run against an empty domain prints DECLARED BUT NOT EXERCISED for every capability it could not reach, because with no record to copy there is no fixture to work on. The full run adds the fixture checks: that Materialize builds exactly one object with a new identity, that a move and your properties read back, that ReleaseMaterialized and Remove leave the world as they found it, and that no test artifact is left behind.

The order to test in

1

Registration

Your adapter is listed, available, with the capabilities you declared. If it is not listed at all, it did not register: the server console logs every refusal and names what was wrong.Then deliberately break your dependency (rename the addon folder, stop DarkRP, whatever applies) and confirm the status flips to unavailable with your sentence, and that Blueprints still starts normally.
2

The read-only contract

vetra_blueprints_adapter test acme.turrets, from any console. It checks your records, your identity floor and your determinism, and writes nothing.
3

The full contract, on a development server

vetra_blueprints_adapter test acme.turrets full, from the server console. Every capability you declared is exercised on a fixture of its own.
4

The real diff

Change one object: move it, or change one property. Capture, and compare against the earlier Version.You want one changed row. If you get one removed plus one added, your identity is derived from something the diff compares, and it will destroy builds. Stop and read Identity.
5

Restart, if you declared persistent

The runner cannot restart the server. This is the check that matters most, and it is yours.
Restart the server process, not a Lua refresh. Capture again and compare against the Version from before the restart.An adapter that declares persistent must produce an empty diff. If objects come back with different ids, you are session, not persistent, and declaring otherwise turns an honest “we cannot match these” into a silent wrong match.
6

The real workflows

The contract proves the terms; the product proves the experience. Migrate a group containing your objects and check the ghost preview lands where you expect. Restore an earlier Version and confirm your objects and their properties come back. Force a partial failure (one member that cannot be built) and confirm nothing is left behind.
7

Missing adapter

Uninstall your addon, restart, and open a Version that contains your records.Your domain must be reported as adapter_missing and your records must still be there. Export that Version, reinstall your addon, and import it back: your records survive the round trip. vetra_blueprints_adapter compat acme.turrets shows the same from the console.
8

Persistence, if applicable

If your domain is backed by a saved record rather than by a live entity, run the full contract and the real workflows, then restart the server and capture again.The fixture must still be gone and every change must still be there. If something reverted, you wrote the entity and not the record. See Persistence integrations.

The checks a first adapter usually fails

The runner reports each of these by name. They are contract terms rather than bugs.
It must return false plus a non-empty reason. Returning nil, or true, is a failure.
{ ok = true, localId = ... } is not enough. identity is required, with source and scope from the declared vocabularies.
A bare { ok = false } is a contract failure, not a refusal. Both code and a non-empty reason are required.
An adapter that creates one thing and releases something else leaves the world different from how it found it.
Phase 1 answers. It does not write, and it does not throw.
A timestamp, an uptime, a recomputed float, or an id that depends on iteration order.
It is source.id, namespaced. Your label is source.metadata.label. This only bites on a path that runs during a real migration or restore, which is why it survives casual testing. See Records.

What no amount of testing will tell you

  • That your domain is complete. Nothing can know whether Collect returned every turret. Only you can.
  • That your identity is really persistent, unless you actually restart the process. A Lua refresh is not a restart.
  • Anything at all, on an empty domain. The runner says DECLARED BUT NOT EXERCISED. Create a few objects first.
Do not ship test hooks, debug convars or fault-injection seams in your adapter. Blueprints does not need them, and a runtime conditional that weakens a contract is a bypass somebody will find.