Commit Graph

5 Commits

Author SHA1 Message Date
Wanjohi
dae2990cbe docs(core): say which database the tests actually need
The helper told you to use "an isolated database for tests", which reads
as a database of its own and is not what the suite wants. Route tests
reach the database through the app and core tests reach it directly, so
two different values put the fixtures in one database and the assertions
in the other — around forty failures, none of them in the code that
caused it, and nothing in the output naming the setting.

Also drops a type import nothing uses.
2026-09-05 18:08:26 +03:00
Wanjohi
9258c8dfef fix(deploy): make bun dev actually start, and sign-in actually work
Six defects found by running the thing rather than reading it. The
previous change was verified by bundling, by tests, and by the container
images — none of which start a Worker, so every one of these was invisible.

**`bun dev` did not start.** It ran one multi-config process, which does
not connect a service binding between the workers it loads; the API
reported `AUTH [not connected]` and could not verify a token. It is two
processes now, which is what the dev registry connects, and the second is
backgrounded with the first killed on exit so stopping the pair stops both.

**Neither server could bind.** Wrangler resolves `localhost` and takes
`::1` first; a host with no IPv6 address on its loopback dies with a bind
error from inside the runtime that names neither the app nor the port.
`dev.ip` is pinned to `127.0.0.1`, and `inspector_port` is now distinct per
app — it is not derived from the port above, so the second server to start
died on an address already in use.

**The API worker failed to evaluate.** A specifier ending in `.sql` is
claimed by the bundler as a module of its own, so the schema file was
emitted verbatim beside the bundle and the runtime threw on an export it
could not find. The route was reaching past the domain module into the
schema to spell a status; it now asks the domain module, which is the rule
everywhere else here and happens to also avoid the hazard.

**Signing in failed on the second request that touched the database.** A
pool is cached per connection string, and on a Worker an I/O object created
while handling one request may not be touched while handling another. The
first request always succeeded, which is why it went unnoticed — a sign-in
is several. The cache is now kept only where a process outlives its
requests, which is the case it was added for.

**The images named a base that podman will not resolve.** A short name
needs a registry; the database service alongside them already spelled one.

**Compose pinned container names.** The name is not scoped to the project,
so a second checkout got the same three, and `down` in one stopped the
other's containers. This is not hypothetical — it stopped a running
development database while this was being tested.

Verified by signing in end to end against both dev servers: a code
requested over HTTP, read from the issuer's log, redeemed, exchanged for
tokens, and presented to the API, which resolved it to the account the
sign-in had just created.
2026-09-05 16:31:04 +03:00
Wanjohi
cf56aaf04c docs: this repo is public, so say what things are, not who decided them
Comments and served API descriptions here had grown references that only make
sense to someone with our internal notes: relative paths that escape this
tree, filenames and titles of documents nobody outside can open, quoted prose
from them, and the name of a component that has no public surface — once in an
OpenAPI description, which is published output rather than source.

None of it was load-bearing. Every case restates as what the code actually
requires, and every rewrite came out shorter: "in the words the host agent
reports" for a component name, "republished as addresses are discovered" for a
quoted phrase, "a size tier sets vCPU, RAM and the output geometry" for a
sentence that had been carrying a path.

Internal reasoning is now cited exactly one way, ref(d-NNNN) in a source
comment, with the rule that the sentence must still stand if the marker is
deleted. CLAUDE.md leads with it, because the previous version of this mistake
was made by people who knew the repo was public and it still took ten
occurrences to notice, so "be careful" is not a mechanism.

Commit messages get the stricter rule and carry no references at all: a
comment can be fixed by the next commit and a published message cannot be
fixed at all. Git hooks now enforce both halves.

The check caught a real one while being written: the CLAUDE.md table spelled
out the paths it was prohibiting, which discloses them to exactly the reader
it protects against.

138 tests, 0 fail.
2026-09-03 22:16:47 +03:00
Wanjohi
6c1d407985 feat(core): a box is a row, a session is the billing unit
Migration 1 of 0048, and the first of the seven weeks — nothing about a live
feed works without these two tables, so it is not a cleanup during them.

  box      a VM someone owns: an id that is also its DNS label, an editable
           label, an owning user, the machine it sits on, a tier and a state.
           Owned by a person and placed on a team's hardware, which are two
           different relationships, hence both userId and machineId.
  session  one run of one box by one linked Steam account, and what costs
           money. Separate from box because the ticket changes after bind as
           addresses are discovered — the vsock contract calls it "a stream,
           not one value" — so it is a column a client polls, not a value it
           is handed once.

Box states are neslet's own three and no more. `starting` and `stopping` are
the obvious additions and both are omitted because nothing would ever write
them; a failed box is `stopped` with stopClean false, which is how neslet
models it too.

The generated migration would have failed on live rows in three ways, so it
is hand-written and tested against a database seeded at the old schema:

  - machine.team_id becomes notNull, and *every existing row is null* because
    the old registration path passed null. Personal teams are backfilled for
    machine owners first, reusing a team they already own rather than minting
    a second, with the owner membership row repaired where missing.
  - game_download.host_id becomes a foreign key. It held free-form strings,
    so unattributable rows are deleted before the cast — the only destructive
    statement here, and a considered loss: it is a progress report neslet
    re-derives from disk.
  - Team.createPersonal was written and documented in packages/core/CLAUDE.md
    as part of the login flow and never actually called, so no user has a
    team. ensurePersonal is idempotent and now runs on every login, which is
    what backfills accounts the migration does not reach.

Verified on a seeded legacy database: three null-team machines backfilled, an
existing team reused rather than duplicated, a blank display name handled, and
both unattributable download rows dropped while the attributable one survived.

Also fixes two things this work ran into rather than caused:

  - Database.client() built a new postgres pool on every call, and use()
    called it twice per invocation — pools of ten connections held for a 30s
    idle timeout. Invisible in a Worker where requests are short; the suite
    crossed 100 connections and Postgres said "sorry, too many clients
    already" in whichever file ran last, which reads as a flaky test rather
    than a leak. Now one pool per connection string.
  - download.test.ts asserted against `hst_…` host ids, which is exactly the
    unattributable row the new foreign key exists to refuse.

There is no "no team" any more: PATCH /machine/:id took teamId null to mean
"mine alone" and now requires a team, because the personal team is the one to
name. Its test is updated to the new contract rather than deleted.

113 → 128 tests, 0 fail.
2026-09-03 21:39:27 +03:00
Wanjohi
3faac3008f feat: Sync to OSS repo 2026-08-06 22:13:51 +03:00