# `Mutare.Sandbox.Command.Invocation`
[🔗](https://github.com/foxbenjaminfox/mutare/blob/v0.5.1/lib/mutare/sandbox/command/invocation.ex#L1)

Spawn `mix` against a materialised sandbox as a fresh OS process.

Every mutant is exercised by its own `mix test` process: the sources never
change between runs, so mix's incremental compiler finds nothing to rebuild
and the per-mutant cost is process boot plus the suite. `MIX_ENV=test` and
the variables selecting the mutant (`Mutare.Selector.environment/1`) are always
set; the rest of a run's environment comes from named **run options**
(`t:run_opts/0`) — an optional wall-clock `:cap` (a mutation can turn a
terminating loop infinite), the compile's own cap and compiler switches, the
coverage probe's capture vars, the heap cap, the original project's root
(`project_root_env/0`), and the per-worker partition entry.

This module implements run invocation: the environment it
runs under (`environment/2`, the one builder every sandbox `mix` goes through;
`mix_env/0`; the self-hosting isolation vars), the raw spawn
(`mix/4`/`timed_mix/4`), and the **timeout enforcement mechanism** — the env var
used to pass the cap (`timeout_env/0`) and the dependency-free watcher AST
(`watcher_ast/0`) that `Mutare.Sandbox` renders into the target's test bootstrap.
The matching half — *decoding* what a run did from its exit code — is
`Mutare.Sandbox.Command`; the codes both halves share are the leaf
`Mutare.Sandbox.Command.Exit`, and `Mutare.Sandbox.Command.timed_test/4`
composes the two (run here, decode there).

## The reserved variable set is the builder's key set

Every variable Mutare sets on a sandbox `mix` is emitted by `environment/2` —
there is no raw env passthrough — so `reserved_env_names/0` is *derived* from it
(the builder run with every option armed), not maintained alongside it. A new run
option cannot be forgotten from the reserved set, because the set is whatever the
builder emits. The one key deliberately outside it is the `:partition` entry: its
name is the user's `:partition_env`, appended last, and the reserved set is what
`Mutare.Options` validates that name against.

## Self-halt watchers

The cap is not enforced by killing a process tree (which needs platform-specific
signals); instead the watcher reads `timeout_env/0` and, after the deadline,
`System.halt/1`s the run itself with `Mutare.Sandbox.Command.Exit.timeout/0`.

A second, structurally identical self-halt guards against the opposite failure:
the *owner* dying rather than the run overrunning. Every run's stdin is a pipe
whose write end only the spawning Mutare process holds, so if that process dies
— however abruptly — the pipe hits EOF. The owner-death watcher
(`owner_watch_ast/0`, gated by `owner_watch_env/0`, rendered by `Mutare.Sandbox`
into the sandbox's config and test bootstrap) blocks reading stdin and halts the
run with `Mutare.Sandbox.Command.Exit.owner_lost/0` as soon as EOF is read,
so no sandbox `mix` outlives the run that spawned it.

# `run_opts`

```elixir
@type run_opts() :: [
  cap: pos_integer() | nil,
  compile_cap: pos_integer() | nil,
  compile: boolean(),
  coverage: {Path.t(), Path.t()} | nil,
  max_heap_mb: pos_integer() | nil,
  schedulers: pos_integer() | :all | nil,
  project_root: Path.t() | nil,
  partition: [{String.t(), String.t()}]
]
```

The named run options every sandbox `mix` is invoked with. Each one maps to the
environment entries `environment/2` emits for it:

  * `:cap` (ms, or `nil`) — the run's wall-clock cap, handed to the injected timeout
    watcher (`timeout_env/0`), which halts the run itself if it overruns.
  * `:compile_cap` (ms, or `nil`) — the **one metamutant compile**'s wall-clock cap
    (`compile_timeout_env/0`), armed only on that invocation.
  * `:compile` (boolean) — the compiler switches that speed the one compile
    (`Mutare.Sandbox.CompilerOptions.compiler_env/0`).
  * `:coverage` (`{dump_path, root}`, or `nil`) — the coverage probe's capture
    flag, dump path and path-normalisation root (`Mutare.Coverage.Recorder`).
  * `:max_heap_mb` (MB, or `nil`) — the per-process heap cap (`emulator_flags_env/1`).
  * `:schedulers` (a count, `:all`, or `nil`) — the run's scheduler threads
    (`emulator_flags_env/1`).
  * `:project_root` (a path, or `nil`) — the directory the sandbox is a copy of,
    expanded to an absolute path (`project_root_env/0`).
  * `:partition` (`[{name, id}]` or `[]`) — the user-named partition entry
    (`Mutare.Runner.Partitions.entry/2`), appended last.

Every option is optional. An absent `:cap`, `:compile_cap`, `:coverage` or
`:project_root` clears its variables explicitly rather than emitting nothing, so a value inherited from
Mutare's own environment cannot enable a watcher or probe omitted from this invocation's
options. The remaining absent options emit nothing.

# `compile_timeout_env`

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

Env var the runner sets to give the **one metamutant compile** its wall-clock
cap (ms).

A sibling of `timeout_env/0` with its own name on purpose: the watcher that
reads it (`compile_watcher_ast/0`) lives in the sandbox `config/config.exs`
prefix, which mix evaluates on *every* sandbox boot — so it must be armed only
when `Mutare.Runner` sets this variable on the compile invocation, and stay
inert on the baseline, the coverage probe, and every per-mutant `mix test`
(whose cap is `timeout_env/0`, armed from the test bootstrap instead).

# `compile_watcher_ast`

```elixir
@spec compile_watcher_ast() :: Macro.t()
```

Dependency-free watcher that enforces the one metamutant compile's wall-clock cap.

The same self-halt watcher as `watcher_ast/0`, armed by `compile_timeout_env/0`
instead: `Mutare.Sandbox` renders it into the sandbox's `config/config.exs`
prefix (mix evaluates config before the compilers run, the same property the
owner-death watcher uses), and `Mutare.Runner` sets the variable only on the
compile invocation — so a thrashing compile halts itself with
`Mutare.Sandbox.Command.Exit.timeout/0` instead of blocking the run
indefinitely, and every other sandbox boot evaluates the watcher inert.

# `emulator_flags_env`

```elixir
@spec emulator_flags_env(run_opts()) :: [{String.t(), String.t()}]
```

The one `ELIXIR_ERL_OPTIONS` entry carrying the emulator flags a sandbox run is
armed with — `:max_heap_mb` and `:schedulers` of `t:run_opts/0` — or `[]` when
neither is armed. One entry for both, since they share the variable.

A pre-existing `ELIXIR_ERL_OPTIONS` in Mutare's own environment is preserved
and the flags appended after it (later emulator flags win), so a user's flags
survive with Mutare's applied on top. With nothing armed the inherited value
reaches the run untouched.

## `:max_heap_mb` — `+hmax <words>`

The emulator's default per-process `max_heap_size`, which **kills the offending
process** when exceeded. This is the memory analogue of the wall-clock watcher, and
like it needs nothing platform-specific: no cgroups, no `ulimit`, no process
tree to hunt down. A mutation that makes code allocate without bound (the
motivating incident: a dropped guard turning a function unconditionally
self-recursive, ~25GB RSS in under a second, OOM-killed) then dies as an
ordinary, fast, attributable test failure inside the run — the growing heap
belongs to the test process exercising the mutant — instead of racing the
kernel's OOM killer for the whole host.

One limitation: `max_heap_size` counts the process *heap* — lists, tuples,
maps, small binaries (the incident's growth shape, and the common one for
runaway recursion). Large (refc) binaries live off-heap and are not counted,
so a pure binary-append runaway is not contained by this cap.

Not applied to the one metamutant compile: the metamutant is ~25× the source
and the compiler's per-process memory is legitimately large — a cap sized for
the suite's runtime could sink the build. The runtime runs (baseline, coverage
probe, every per-mutant `mix test`) all get it — the baseline doubles as
validation that the suite itself fits under the cap, so a too-small value
surfaces as a red baseline up front rather than as false kills mid-run.

## `:schedulers` — `+S <n>:<n>`

Trims the run's BEAM to `n` scheduler threads, all online. `:workers` such runs
execute at once, and an untrimmed BEAM takes every core for itself, so without
this the per-mutant phase oversubscribes the CPU by the worker count. Dirty CPU
schedulers follow the count, and so does whatever the suite derives from
`System.schedulers_online/0` — ExUnit's default `max_cases` above all, so a
trimmed run executes fewer async tests at once. `:all` (and `nil`) emit no flag.

The baseline runs under the same count as the mutants, so it checks the suite is
green at the concurrency they get, and times it at their speed — the per-mutant
cap is a multiple of that time. The one compile and the coverage probe run alone
on the machine and are not trimmed.

# `environment`

```elixir
@spec environment(Mutare.RuntimeId.t(), run_opts()) :: [
  {String.t(), String.t() | nil}
]
```

The full environment a sandbox `mix` for `mutant_id` runs under, given its run
options (`t:run_opts/0`) — the one builder `mix/4` uses and `reserved_env_names/0`
derives from.

Always present: `MIX_ENV`, the owner-death watcher's arming variable, the two
self-hosting isolation variables, and the selector variables for `mutant_id`. Then
one group per armed option, in `t:run_opts/0` order, and the `:partition` entry
last. Pure apart from reading Mutare's *own* environment where an entry merges
with an inherited value (`emulator_flags_env/1`, `CompilerOptions.compiler_env/0`).

# `mix`

```elixir
@spec mix(Path.t(), [String.t()], Mutare.RuntimeId.t(), run_opts()) ::
  {String.t(), non_neg_integer()}
```

Run `mix <args>` in `sandbox` as a fresh OS process, returning
`{output, exit_status}`.

The environment is `environment/2` of `mutant_id` and `opts` (see `t:run_opts/0`).
`mutant_id` is a runtime identity (`Mutare.Selector.baseline/0` for a baseline
run); for a schema mutant, `Selector.environment/1` splits its `{file, local_id}`
into the namespace and integer environment variables, and integer calls clear any
inherited namespace.

# `mix_env`

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

The `MIX_ENV` every sandbox `mix` runs under (`"test"`). The single home for the value,
so `Mutare.Sandbox` (which builds `_build/<env>/lib` paths) and `Mutare.Transform.Uses`
share it rather than re-hardcoding the string.

# `owner_watch_ast`

```elixir
@spec owner_watch_ast() :: Macro.t()
```

Dependency-free watcher that halts a sandbox run whose owner died.

Reads `owner_watch_env/0`: unset (a manual run in a kept sandbox, CI with a
closed stdin) it is inert, otherwise it spawns a process that blocks reading
stdin and, on `:eof`, `System.halt/1`s the run with
`Mutare.Sandbox.Command.Exit.owner_lost/0`. Under `mix/4` the run's stdin is a
pipe whose write end only the owning Mutare process holds; when that process
dies — clean exit, crash, or SIGKILL (the kernel closes its descriptors) — the
pipe hits EOF and the run reaps itself, instead of surviving re-parented (a
compute-bound `mix` never touches stdout, so it would otherwise run on
untouched). `Mutare.Sandbox` renders this AST into the sandbox's
project prefix, `config/config.exs` (mix evaluates config before compiling, so the one-time
metamutant compile and every run's boot phase are covered) and into the test
bootstrap alongside `watcher_ast/0` (covering suites under a config layout the
config injection can't reach). Like the timeout watcher, it needs nothing
platform-specific and no dependency on Mutare — the same self-halt primitive
pointed at a second hazard. Only the first armed evaluation starts a watcher.

No data is sent on stdin under `mix/4` (Mutare writes nothing to the pipe),
so the watcher simply re-blocks on anything that isn't `:eof`; a target suite
that reads stdin receives the same input as without the watcher —
a silent, open pipe.

# `owner_watch_env`

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

Env var that arms the owner-death watcher (`owner_watch_ast/0`).

Set by `mix/4` on every sandbox run, and only there: the watcher halts the run
the moment stdin hits EOF, which is the owner-died signal *only* when stdin is
the spawning process's pipe. A sandbox `mix` run by hand (or by CI with stdin
at `/dev/null`) must stay unaffected, so the watcher is inert unless this
variable is set.

# `project_root_env`

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

Env var naming the absolute path of the directory the sandbox is a copy of
(`Mutare.Project`'s `copy_root`: the umbrella root, or the single project's root).

The sandbox lives under the system temp dir and carries no `.git`, so a target
config that derives something from where it lives — a per-worktree test database
named from the checkout's path or branch — reads the sandbox instead of the
checkout. This is the target's way back: `System.get_env("MUTARE_PROJECT_ROOT")`,
falling back to its usual derivation outside Mutare. The runner sets it (the
`:project_root` run option) on every sandbox `mix` it spawns, the one compile
included, since `mix compile` evaluates the target's config too.

# `reserved_env_names`

```elixir
@spec reserved_env_names() :: [String.t()]
```

The environment variable names Mutare itself sets on a sandbox `mix`: the keys of
`environment/2` with every run option armed (`:partition` excepted — its name is
the user's, and this set is what it is validated against).

Derived, not listed: a run option added to `environment/2` is reserved by
construction. `Mutare.Options` rejects a `:partition_env` that collides with one
of these, since the partition entry is appended to the environment and a
duplicate key's resolution is unspecified (it would silently clobber e.g.
`MIX_ENV`).

# `timed_mix`

```elixir
@spec timed_mix(Path.t(), [String.t()], Mutare.RuntimeId.t(), run_opts()) ::
  {non_neg_integer(), String.t(), non_neg_integer()}
```

Like `mix/4`, but wall-clock-timed: returns `{elapsed_ms, output, exit_status}`.
`opts` are the run options (`t:run_opts/0`), passed straight through.

# `timeout_env`

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

Env var the runner sets to give a mutant run its wall-clock cap (ms).

# `watcher_ast`

```elixir
@spec watcher_ast() :: Macro.t()
```

Dependency-free watcher that enforces a mutant run's wall-clock cap.

Reads `timeout_env/0`: with no cap it is inert, otherwise it spawns a process
that sleeps for the cap and then `System.halt/1`s the run with
`Mutare.Sandbox.Command.Exit.timeout/0` — so the run halts *itself* and there is
no process tree to kill. `Mutare.Sandbox` renders this AST into the target
project prefix and test-bootstrap fallback, alongside `Mutare.Selector.bootstrap_ast/0`,
so the target needs nothing platform-specific and no dependency on Mutare.
The first armed evaluation starts the deadline; later prefixes cannot reset it.

---

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