This command had no tests at all, while carrying the rules that decide whether
a deploy may change a published agent. Everything interesting was inside run(),
which loads configuration, opens its own pool and resolves a tenant from a
slug — none of which a test can supply. So it was untestable by construction
rather than by neglect, and the fix is a seam, not a test-only helper.
Three functions come out of run(), each doing one thing:
validateSpecs the parse and status checks. Pure.
validateGraph §3's DAG check over the whole set. Pure.
importInto the write phase, taking a transaction the caller owns and
returning what it did.
run() is now the wiring around them. importInto does not commit — the caller
does — so a refused rewrite leaves the caller's deferred rollback to undo the
writes that already happened, which is the behaviour that was there before and
is now visible in the signature rather than implied by where the code sat.
Six tests, four of them against a real database:
- every problem is reported, not the first: two bad specs produce two
messages and a good one produces none;
- a chain is not a cycle, and a cycle names the edge to cut;
- a first import records versions, and a second over unchanged specs
records none — the counter that used to say nine every deploy;
- a changed spec at the same version is refused AND nothing is committed,
checked by reading the row back;
- a lowered version is refused and the live row is still at the higher one;
- a raised version is accepted and leaves two rows in the history.
The author is resolved through resolveAuthor rather than passed as a literal,
so the tests exercise that path too and fail loudly on an organization with no
active admin — a real deployment condition. The first draft passed "" and got
`invalid input syntax for type uuid`, which is what a literal buys you.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g