fix(api): a run drives the box under it, and needs a game and a claim

Four things the session endpoints did not do, or did wrongly.

The box had three states and nothing wrote them. A box read `created`
while a run on it was `live`, so every screen showing a person what their
hardware is doing was reading a column no code had ever moved. A run
reaching `live` now makes its box `running`, and a terminal run stops it:
`ended` cleanly, `failed` not, carrying the reason the agent gave. Not
every run state maps — a box has no `starting` on purpose, because that
transition is synchronous from the agent's side and a state nobody sets
is a state that lies. Both writes are one transaction, since "this run is
live" and "the box under it is running" are one fact in two tables, and a
box stuck `running` with nothing on it has nothing to correct it.

`POST /session` accepted any game in the catalog. A run launches as a
Steam account that has to own the game, so one outside the caller's
library is a box that starts, tries to launch and fails minutes later
with nothing to point at; it is now refused up front. Told apart from a
game that does not exist rather than hidden, because the catalog is
public and "you do not own this" is a sentence a person can act on. The
library is a synced copy, so this refuses a game bought since the last
sync — that is a staleness bug in the sync, not a reason to start runs
that cannot work.

Publishing a ticket only refused terminal runs, so a host could publish
an address for a run it had never claimed. A ticket is the address of
something being brought up, so only `starting` and `live` accept one, and
the state is in the write rather than only in the check above it. The two
refusals stay separate answers because they are different mistakes: one
agent skipped a step, the other has nothing left to reach.

The migration that adds the one-active-run index stopped older duplicate
runs without clearing the ticket they had published, which is the
invariant that same migration exists to establish. It clears it now,
verified against a box carrying two unstopped runs.

Nine tests, each checked against the unfixed code first.
This commit is contained in:
Wanjohi
2026-09-04 22:12:31 +03:00
parent 0d8630379b
commit 51dabddbd8
5 changed files with 316 additions and 25 deletions

View File

@@ -15,8 +15,14 @@
--
-- `ended` and not `failed`: nothing about these runs failed. They were work
-- nobody picked up, and `failed` carries a reason there is none of.
--
-- The ticket goes with the state, exactly as the terminal transitions in
-- `Session` do it. A duplicate that reached `starting` or `live` may have
-- published an address, and a stopped run that still answers with one is an
-- address a polling client would dial — with publishing a replacement already
-- refused, it would also be the last word.
UPDATE "session" s
SET "state" = 'ended', "time_stopped" = now()
SET "state" = 'ended', "time_stopped" = now(), "ticket" = NULL
WHERE s."time_stopped" IS NULL
AND s."time_deleted" IS NULL
AND EXISTS (