# `DrinkMeAlice.TestServer`

Owned, private, loopback-only SurrealDB server child — for tests of
this library and of anything built on it.

- Data lives under a caller-provided private directory (`<dir>/data`,
  SurrealKV); the directory is created mode 0700.
- Root credentials are freshly generated per server, passed only via the
  `SURREAL_USER`/`SURREAL_PASS` environment (never on the command line),
  and never written to logs or receipts.
- Bound to an ephemeral loopback port picked by the kernel.
- The server's full version identity is recorded as the first line of
  `<dir>/server.log` — a bare version number does not identify an
  artifact.
- Port (stderr) output is appended to `<dir>/server.log` with a real
  cumulative byte cap (262144); `server.log_bytes` tracks it.
  Long-running probes should call `drain/2` inside their wait loops so
  diagnostic bytes never accumulate in the port mailbox.
- Startup reports definite failure on early child exit (including bind
  failure); a `/health` 200 counts only while the owned child is alive.
- Concurrent starts on the SAME data dir are refused, never raced.
  Within this VM a per-dir claim — a dedicated holder process per
  start, monitoring the port owner, so one caller may hold several
  stores while a claim still dies with its owner — makes the second
  caller fail fast with `{:error, {:data_dir_in_use, dir,
  holder_pid}}`. Against an already-existing store, the SurrealKV
  endpoint is scanned for a SUSPECTED foreign `surreal start`
  (typically a leftover from a killed VM) and refused with
  `{:error, {:data_dir_lock_suspected, dir, suspects}}` BEFORE any
  child is spawned or the old log is touched. The scan is
  best-effort, fail-open, and suspicion is a hint to verify — not
  verified lock ownership; an early exit that still slips through
  its race window is reported with the log tail and the suspected
  foreign holders.
- `stop/1` escalates TERM -> KILL under finite deadlines and confirms the
  OS child is reaped; the last observed exit status is preserved, and
  the data-dir claim is released — only for THIS instance's claim,
  only on proven cleanup, and SYNCHRONOUSLY: normally `stop/1` does
  not report completion before the registration is removed, so an
  immediate same-dir restart is not refused by our own stale claim.
  If the bounded release handshake times out (a holder that cannot
  run), the result preserves both facts as
  `{:error, {:claim_release_unconfirmed, holder, cleanup}}`. A
  stale handle or an unproven cleanup cannot strip a live claim; a
  FAILED start with unproven child cleanup (`{:acquisition_unclean,
  reason, holder}` or `{:cleanup_unproven, other, holder}`) retains
  its claim — with the holder pid in the error as a receipt — until
  the starting process dies.

## Version qualification

The suite is written and qualified against SurrealDB
3.0.5 ONLY. `start/1` resolves the executable
(`SURREAL_BIN`, else `surreal` on PATH), records its identity and
refuses a different version with `{:error,
{:unqualified_server, reason}}` — a later 3.x build is not
automatically the qualified behavior. Set `SURREAL_ALLOW_UNQUALIFIED=1`
to start anyway: that is a compatibility EXPERIMENT, not a
qualification, and every such run says so on stdout.

# `creds`

```elixir
@type creds() :: %{user: String.t(), pass: String.t()}
```

# `t`

```elixir
@type t() :: %DrinkMeAlice.TestServer{
  claim_pid: pid() | nil,
  creds: creds(),
  dir: String.t(),
  exit_status: integer() | nil,
  http_port: :inet.port_number(),
  log_bytes: non_neg_integer(),
  log_path: String.t(),
  os_pid: non_neg_integer(),
  port: port()
}
```

# `drain`

```elixir
@spec drain(t(), non_neg_integer()) :: t()
```

Drain port output into server.log and return the updated server.

Always performs a nonblocking burst pass first (bounded at
512 queued items) so an already-queued exit_status is
observed even with `timeout: 0`; then blocks up to `timeout` for trailing
output. Call inside long wait loops so diagnostic bytes never accumulate
unboundedly in the port mailbox.

# `live_policy`

```elixir
@spec live_policy() :: :ok | {:skip, String.t()} | {:error, String.t()}
```

Whether `:live` tests may run: `:ok`, `{:skip, reason}`, or — with
`SURREAL_REQUIRE_QUALIFIED=1` set (CI) — `{:error, reason}` for
anything that would otherwise be skipped, so a qualification job fails
instead of silently excluding its live suite. A different installed
server version skips with the reason; set
`SURREAL_ALLOW_UNQUALIFIED=1` to run it anyway as a compatibility
experiment whose results are NOT a qualification.

# `parse_version`

```elixir
@spec parse_version(binary()) :: {:ok, String.t()} | :error
```

The first `major.minor.patch` in a version line, or `:error`.

# `qualified_version`

```elixir
@spec qualified_version() :: String.t()
```

The SurrealDB version this suite is qualified against.

# `resolve_bin`

```elixir
@spec resolve_bin() :: {:ok, binary(), binary()} | {:error, term()}
```

`{bin, identity}` for the SurrealDB executable: `SURREAL_BIN` if set,
else `surreal` on `PATH`. `identity` is the FULL `surreal version`
output — build metadata included, because a version number alone does
not identify the artifact under test.

The version probe is bounded (2000 ms) and
typed: a missing binary, an unusable path, a nonzero exit or a hung
command is `{:error, reason}` — never a raise, never a parsed
substring of failed output.

# `start`

```elixir
@spec start(keyword()) :: {:ok, t()} | {:error, term()}
```

Start a private server. Options:

  * `:dir` (required) — private root; `<dir>/data` is the SurrealKV path,
    `<dir>/server.log` records the server identity and bounded stderr.
  * `:port` — bind this port instead of a free one (restart on the
    same endpoint, so a conn configured once reconnects to it).
  * `:user` / `:pass` — reuse these root credentials instead of
    generating fresh ones. REQUIRED when restarting onto an existing
    SurrealKV store: the stored root user persists, so fresh creds
    would not take effect and every signin would fail -32002.
  * `:args` — extra `surreal start` CLI args (e.g. `--allow-net`,
    `--async-event-interval`); must never contain credentials.
  * `:env` — extra env pairs (charlist keys/values).
  * `:health` — `false` to skip the /health wait.

A caller may hold several servers at once (each start claims its own
directory through a dedicated holder process). Refuses with
`{:error, {:unqualified_server, reason}}` before creating anything
when the resolved executable is not the qualified version and
`SURREAL_ALLOW_UNQUALIFIED=1` is not set. Refuses with
`{:error, {:data_dir_in_use, dir, holder_pid}}` when another live
claim in this VM already owns `dir`. When the store already exists,
a best-effort host process scan runs: a SUSPECTED foreign holder (a
process whose executable is named `surreal` and whose arguments
carry the exact endpoint token) refuses with
`{:error, {:data_dir_lock_suspected, dir, suspects}}` before any
child is spawned. A command-line match is a hint, not verified lock
ownership: identify the process, confirm it really owns this store,
and stop that owner — do not kill on a command-line match alone, and
never delete a store that may still be active. The scan is
fail-open. Directories are claimed by their expanded path. On error,
pre-spawn failures release the claim synchronously (if that release
cannot be confirmed, the result says so via
`{:claim_release_unconfirmed, holder, reason}`); a start whose child
was acquired but not proven cleaned up retains the claim — with the
holder pid in the error as a receipt — until the starting process
dies. Diagnostics are best-effort (a broken IO device cannot prevent
finalization), and EVERY exception after acquisition — including one
raised by failing diagnostics inside a rescue handler — is converted
to proven cleanup or a retained claim with a typed receipt
(`{:startup_failed, {:finalization_crashed, cause}, ...}`).

# `stop`

```elixir
@spec stop(t()) ::
  {:exit_status, integer()} | {:reaped, :status_unobserved} | {:error, term()}
```

Stop the server: drain queued port messages, TERM, bounded grace, KILL,
drain again, confirm reaped — then release the data-dir claim so the
documented restart-on-the-same-dir flow keeps working.

The claim is released ONLY when cleanup is proven AND this instance
still owns the claim: the live registration must be this start's
holder process, so a stale handle stopped after a replacement start
cannot release the replacement's claim, and an unproven cleanup
(`{:error, :unreaped}` and friends) leaves the exclusion in place
while the child may still hold the store — it clears when the owning
process dies.

Release itself is a bounded synchronous handshake: normally `stop/1`
does not return until the registration is removed. If confirmation
times out (a holder that cannot run), the public result preserves
BOTH facts — the child cleanup proof and the unresolved release —
as `{:error, {:claim_release_unconfirmed, holder, cleanup}}`; the
claim clears when the holder next runs or its owner dies, and an
immediate same-directory restart may still be refused until then.

Returns `{:exit_status, n}` only for an observed OS exit code;
`{:reaped, :status_unobserved}` when the child is confirmed gone but no
exit_status message was observed; `{:error, ...}` when cleanup cannot be
proven or the claim release is unconfirmed.

# `surreal_bin`

The SurrealDB binary path, raising when absent (deprecated raise
behavior kept for direct callers; prefer `resolve_bin/0`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
