Run the contract
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.
Reading the report
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
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
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.ApplyTransform does not refuse an unknown id properly
ApplyTransform does not refuse an unknown id properly
It must return
false plus a non-empty reason. Returning nil, or true, is a failure.Materialize does not return an identity table
Materialize does not return an identity table
{ ok = true, localId = ... } is not enough. identity is required, with source and
scope from the declared vocabularies.A phase-1 refusal carries no code or reason
A phase-1 refusal carries no code or reason
A bare
{ ok = false } is a contract failure, not a refusal. Both code and a non-empty
reason are required.ReleaseMaterialized does not restore the object count
ReleaseMaterialized does not restore the object count
An adapter that creates one thing and releases something else leaves the world different
from how it found it.
A Prepare function writes
A Prepare function writes
Phase 1 answers. It does not write, and it does not throw.
Two consecutive captures disagree
Two consecutive captures disagree
A timestamp, an uptime, a recomputed float, or an id that depends on iteration order.
The adapter reaches for source.localId
The adapter reaches for source.localId
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
Collectreturned 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.