Files
benilla-octo/crates/benilla-app/src/run_mode.rs
T
SamandClaude Fable 5.1 e259854a4d an item on the cursor is a sale, and an animation's sound has a place
- right-clicking a vendor with an item on the cursor sells it: `0x5df5d0` asks `GetCursorItem` before anything else and a non-zero answer takes the arm to `CMSG_SELL_ITEM` instead of `CMSG_LIST_INVENTORY`, where benilla sent the list unconditionally — the whole stack, to the clicked vendor, with no merchant-window gate anywhere in the arm; the clear is `SetCursorItem(0, …)` rather than `ClearCursor`, which fires `CURSOR_UPDATE` and leaves the source slot greyed until the server's inventory update (1914); and the NPC you are talking to is not clickable: `0x5f0130` refuses a right-click on the NPC whose window is already open, against a global with exactly two writers — the interaction setter, whose fourteen callers are every one a window opener, and its clear — so the first click always gets through and no double-click is eaten, where benilla re-sent the arm's opcode on every click (1866)
- a Hated or Hostile questgiver's `!` now disappears, every `!` blinks off and back on when you resurrect, and the minimap no longer dots a corpse, your own pet or an untrackable unit: the sweep tears down on HOSTILITY, not on the flag — `0x5eb134` is a convergent block reached from two branches, so three incompatible readings had each seen one path — and an escort giver's `!` clearing with its flag is the field watch instead, which B257 now rests on; the dots gain `0x4eaa90`'s three preconditions (1906); the quest log's rebuild is all-or-nothing — the reference makes every rebuild a no-op until its template misses are answered, so a cold log shows empty and then lands in one step, never our `"..."` row per in-flight quest — sorted level then title, case-insensitively, with the `id == 0` group first under the reference's own "Missing header! (quest designers)"; and `GetQuestLogTitle`'s `isHeader`/`isCollapsed` answer 1/nil, not booleans
- an animation event has a place, not just a time: the M2 event kernel (`0x719370`) places each fired key through its bone, and benilla carried the when and the what and never the where, so every animation-event sound played at the model root — 149 of 244 `$DSL` records sit off their model's origin, out to 67.6 yd on `Maraudon_Waterfall01.m2` — and the point belongs to the record, not the tag, since 75 models author one tag twice; doodad, GameObject and shake sounds now sound where the key is (1904); and the unit dispatcher's arms do not agree about the point they are handed — footsteps and emotes at the event point (the footstep's material foley stays at the origin), `$TRD` at the origin, `$CSD` at attachment 17 rather than its head-mounted records, `$CSS` at the event point on a hit and attachment 1 on a miss — with `0x623b90`'s attachment-else-bias resolver transcribed table and all (1915)
- `GetSkillLineInfo` out of range answers the reference's thirteen-value tuple, numbers in the rank slots, not a single nil — the stock skill window adds two of those slots unguarded straight off `SetSelectedSkill(0)`, so one nil raises the moment it drives us; one shape serves every out-of-range case because the bounds test at `0x4d3675` is a single unsigned compare, and our source had asserted the opposite (1919); the outbound wire is traced by name (`out`, mirroring `in`), a line pushed only after a successful write — a 5-second visit to the Goldshire poster reads as 87 lines (1901); the binding shape gate seats a player and checks the `Unit*` verbs it had been skipping, coverage 220 → 249, all agreeing with the reference (1911); and a UI capture scenario opts the UI in itself instead of photographing a UI-less world and exiting 0, the opt-in answering `false` in a player build that has no harness

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:51:17 +07:00

523 lines
26 KiB
Rust

//! **What kind of run is this, and whose** — the always-present layer, on the player's side of the
//! `dev` seam (decisions 0026 / 1173, built in 1174).
//!
//! Everything here answers a question about *the run itself*, and every answer has a
//! **player-faithful default**: nobody is driving the camera, the client starts at the login
//! screen, no rig owns the character pick, this checkout belongs to nobody in particular. A dev
//! build's instruments then move those answers off their defaults — which is the one direction the
//! seam allows. Gameplay reads this module; gameplay never reads `capture`, `debug_panel` or
//! `perf`.
//!
//! It exists because the alternative kept losing. 0026 asked for "a player-safe home or default"
//! for exactly this class of fact in June 2026 and named two of them; by August there were 24
//! references from non-dev code into the instruments (1173), because the facts had no home and the
//! nearest thing that already knew the answer was the instrument itself.
//!
//! The engine drew the same line for itself one layer down: [`benilla_world::dev_state`]'s
//! `deterministic_run` reads `$WOW_CAPTURE` on its own account, because "run deterministically" is
//! a property of the world and the world must be able to ask it with no harness above it at all.
//! **One environment variable, several readers, no shared symbol** — that is the pattern, and
//! [`scenario_active`] below is its app-side twin. Keep the two in step.
use bevy::prelude::*;
/// **Something other than the player is authoring the camera and the avatar this run.**
///
/// Inserted by the capture harness, which is the only thing that ever drives them; a player build
/// compiles no inserter, so the resource simply never exists and every `run_if` below reads as
/// "the player is in charge" for free. The *type* lives here rather than in `capture` for one
/// reason: `player::control`, `player::server_ride`, the self-model fade and the UI pass all name
/// it in a run condition, and a run condition that names a dev type is a dependency on dev.
#[derive(Resource)]
pub(crate) struct CaptureMode;
/// The rig's derived character name (decision 0651), when `$WOW_RIG` names a body.
///
/// Inserted by `capture::ProbeRigPlugin` at build time; absent otherwise — which is the player
/// answer, and the answer in any run the rig is not driving. The character-select roster reads it
/// to know that the pick is already spoken for, so it must not also honour `$WOW_CHAR`: the rig may
/// have to *create* its body first, and the `WOW_CHAR` fast path is a one-shot that structurally
/// cannot wait for that. Before 1174 the roster asked the harness directly.
#[derive(Resource)]
pub(crate) struct RigCharacter(pub(crate) String);
/// **Is a capture running?** (`$WOW_CAPTURE` set.)
///
/// Read before any plugin builds — it decides whether the net thread starts, what size the window
/// opens at, and whether clutter/anim-LOD randomness is pinned. All of those are properties of the
/// run, not of the harness, and all of them must have an answer in a build with no harness in it.
/// See the module doc for why this reads the variable rather than asking `capture`.
pub(crate) fn scenario_active() -> bool {
std::env::var("WOW_CAPTURE").is_ok()
}
/// **Is the player UI opted into this capture?** — the harness's [`crate::capture::ui_opted_in`],
/// forwarded from here for the same reason [`scenario_active`] lives here: three consumers outside
/// the harness need the answer (the UI load, the synthetic unit feed, the window size), and **all
/// of them must still compile in a build with no harness in it**. `mod capture` is
/// `#[cfg(feature = "dev")]`; this module is not.
///
/// `false` in a player build is the correct answer and not a stub: that binary has no scenario
/// table and no `$WOW_CAPTURE` handling, so there is no capture for the UI to be opted into.
#[cfg(feature = "dev")]
pub(crate) fn capture_ui_opted_in() -> bool {
crate::capture::ui_opted_in()
}
#[cfg(not(feature = "dev"))]
pub(crate) fn capture_ui_opted_in() -> bool {
false
}
/// **Do the credentials come from the environment?** (`$WOW_USER` / `$WOW_PASS` / `$WOW_CHAR` —
/// the login screen's env fast path.)
///
/// The player answer is `false`, and it is the default: with none of these set the client opens at
/// the login screen and waits for somebody to type. Setting any of them says *"log in without
/// waiting for me to type"* — and **that is the only thing it says.** It is not a claim about who
/// is in the room; [`unattended`] is the fact for that, and keeping the two apart is decision 1769.
///
/// One environment fact, several readers, no shared symbol — the module doc's pattern, same as
/// [`scenario_active`], and the reason the roster's `WOW_CHAR` fast path asks here instead of
/// re-reading the three names.
pub(crate) fn env_login() -> bool {
["WOW_USER", "WOW_PASS", "WOW_CHAR"]
.iter()
.any(|k| std::env::var_os(k).is_some())
}
/// **Is there nobody here?** (`$WOW_UNATTENDED`, or the two runs that are automated by
/// construction: `$WOW_CAPTURE` and `$WOW_RIG`.)
///
/// The player answer is `false` and it is the **default**, like every other answer in this module:
/// a client with nobody at the keyboard is the exception, and an exception declares itself. The
/// direction is the whole point — guessing "attended" when a probe is running costs that probe a
/// timeout, loudly; guessing "unattended" when a person is playing ships them a client that fights
/// another client for their account, silently. Those two are not worth the same, so the doubt goes
/// one way.
///
/// **This is the fact [`env_login`] was mistaken for, twice.** Env credentials mean "log in without
/// typing"; they say nothing about the room — and the director plays with
/// `WOW_USER=one WOW_PASS=pone cargo play`, which is the example line in `.cargo/config.toml`. So
/// every session they played read as "a harness": a typed password's refusal killed the process
/// (fixed on 2026-08-28 by asking `announced` — direct evidence — instead of the environment) and,
/// the half that fix left standing, a kick sent the client racing to take the account back instead
/// of to the login screen. That is decision 1262's reported bug, reappearing three weeks later
/// with not one line of its code changed, because the fact underneath it was never true. 1769.
///
/// `WOW_CAPTURE` and `WOW_RIG` are folded in because they *cannot* be a person: a capture authors
/// the camera and a rig drives the body, so a run that sets either has already said what it is and
/// cannot forget to. Everything else declares — `scripts/leg.sh`, `smoke.sh`, `cine.sh`,
/// `summon-live.sh` and the ad-hoc probe recipe in `method.md` all pass `WOW_UNATTENDED=1`.
///
/// Read by whatever may act *instead of* a person: the lost-session verdict (decision 1262 — the
/// session-loss readers never call this directly, [`crate::net::DisconnectedMessage::new`] asks
/// once at the wire edge and every reader acts on the verdict it carries) and the two `FATAL`
/// exits that keep a driverless run from burning its wall-clock on a dialog (decision 1371).
pub(crate) fn unattended() -> bool {
["WOW_UNATTENDED", "WOW_CAPTURE", "WOW_RIG"]
.iter()
.any(|k| std::env::var_os(k).is_some())
}
/// **This run cannot get past the screen it is on — may the client end it instead of a person?**
/// (decision 1371's `FATAL` marker, resting on 1769's fact.)
///
/// `true` only for a run that has declared itself [`unattended`]: it exits non-zero on the one
/// greppable marker `scripts/leg.sh` keys on, rather than parking a driverless run on a dialog for
/// its whole wall-clock. Otherwise the dialog stays up for whoever is there.
///
/// The `false` arm still speaks, when the credentials came from the environment: a probe that
/// quietly parks for 300 s instead of failing in 5 is precisely what 1769's default trades away,
/// so the log names the declaration that buys the old behaviour back. Both arms live here because
/// three call sites — two in [`crate::login`], one in [`crate::char_select`] — must not drift
/// apart on either the verdict or the marker `leg.sh` greps for.
pub(crate) fn fatal_when_driverless(why: &str) -> bool {
if unattended() {
error!("login: FATAL — {why}; exiting");
return true;
}
if env_login() {
warn!(
"login: {why} — leaving the dialog up, because nothing declared this run driverless. \
Set WOW_UNATTENDED=1 if nobody is here and it should exit non-zero instead \
(decision 1769)."
);
}
false
}
/// **Which screen the client starts on.** A player starts at the login screen, always; a capture
/// boots straight into the world (no net, no picker), and a *glue* capture boots onto the very
/// screen it photographs.
///
/// The one place this module forwards into the dev half, and deliberately the only one: the answer
/// is needed *before the plugins build* (it is `CharSelectPlugin`'s `start`), so it cannot be a
/// resource an instrument inserts the way [`CaptureMode`] and [`RigCharacter`] are. The forward is
/// one call, in one direction, and the player arm names nothing at all.
pub(crate) fn start_state() -> crate::char_select::ClientState {
#[cfg(feature = "dev")]
{
crate::capture::start_state()
}
#[cfg(not(feature = "dev"))]
{
crate::char_select::ClientState::Login
}
}
/// **Does this build offer developer affordances at all?** `false` in a player build.
///
/// The generalisation of 1176's one-door rule, and the thing that record should have written
/// (decision 1179). A dev affordance does not have to live in a dev module: the world map's
/// Alt-click jump is in `ui_world_map`, the `/castvis` family is in `ui_chat`, the dev key plane's
/// cost is in `bindings`. None of them names a dev root, so none of them fails to compile, and all
/// of them shipped to players in 1174 exactly as free-fly did.
///
/// So the question a gameplay module asks is not "is the debug panel compiled in" — it is **"may I
/// offer this at all"**, and there is one place to ask it. Held by
/// [`tests::the_dev_plane_has_exactly_one_door`].
pub(crate) fn dev_affordances() -> bool {
cfg!(feature = "dev")
}
/// **Did a dev-chord affordance just fire?** `Ctrl`+`Shift`+*key*, and `false` in a player build.
///
/// The one door onto the dev plane, and the reason it exists: the plane is
/// [`benilla_world::modkeys::dev_chord`], which lives in the **engine** (1160 moved it there —
/// "nothing about which two modifiers are the dev plane is a debug-panel opinion"), and the engine
/// is always compiled. So a gameplay module asking for a dev chord creates **no symbol into a dev
/// module**, compiles clean with `--no-default-features`, and ships a live dev affordance to a
/// player. That is exactly what happened: 1174 landed a green player build in which `Ctrl+Shift+F`
/// still flew the camera through the world, `+G` still teleported the avatar, and `+M` still muted
/// the game (decision 1176).
///
/// Routing every non-dev reader through here makes the whole plane dark at once rather than five
/// keys at a time, and makes the next dev chord dark for free. The build gate cannot see this class
/// — nothing fails to compile — so it is held by [`tests::the_dev_plane_has_exactly_one_door`]
/// instead, in the suite the gates already run.
pub(crate) fn dev_chord(keys: &ButtonInput<KeyCode>, key: KeyCode) -> bool {
dev_affordances() && benilla_world::modkeys::dev_chord(keys, key)
}
/// The free-fly hint the take-control line carries, or empty when there is no chord to advertise.
/// A player build must not offer a key that does nothing. Spelled from
/// [`benilla_world::modkeys::DEV_CHORD`] like every other surface that names the plane, so it
/// cannot drift from what the chord actually listens for.
pub(crate) fn free_fly_hint() -> String {
#[cfg(feature = "dev")]
{
format!(
" ({} + F toggles free-fly)",
benilla_world::modkeys::DEV_CHORD
)
}
#[cfg(not(feature = "dev"))]
{
String::new()
}
}
/// **This crate's source directory on a dev build, `None` in a player build** — the seam's second
/// door, and the one [`dev_affordances`] cannot be.
///
/// `dev_affordances()` answers "may I offer this", at runtime. Part of the seam is not about
/// offering anything: it is about a **string being absent from the binary**.
/// `env!("CARGO_MANIFEST_DIR")` names the build machine's home directory, and a runtime `if` does
/// not remove it — the literal is still compiled in, still in `strings`, still shipped. 1174's
/// pool-slot guard made exactly that mistake one level down ("inert in a player build" read as
/// "absent from a player build"), and decision 1175 is entirely about a binary that stops
/// depending on the machine that built it. So this `cfg` has to be real, and it lives here, once,
/// behind a value instead of scattered across the modules that need it.
///
/// Two callers, both resolving content or state and neither of them an affordance:
/// [`crate::ui_script`]'s dev probe into `assets/ui` (so editing FrameXML costs no recompile) and
/// [`crate::local_state`]'s project-folder home. Both get `None` in a player build and fall through
/// to the compiled-in copy and to `<exe dir>` respectively, with no path literal in the binary.
///
/// Held by [`tests::the_dev_plane_has_exactly_one_door`] like the rest of the seam.
pub(crate) fn dev_source_dir() -> Option<&'static std::path::Path> {
#[cfg(feature = "dev")]
{
Some(std::path::Path::new(env!("CARGO_MANIFEST_DIR")))
}
#[cfg(not(feature = "dev"))]
{
None
}
}
/// The pre-connect **account guard**, consulted by the login policy's env fast path
/// ([`crate::login`]). Returns `Err(explanation)` when this build lives in a worktree pool slot and
/// the fast path is about to authenticate as an account that belongs to somebody else — the
/// director's `one` (a login KICKS their live session mid-play) or another slot's `probeN` (the
/// kicked client's 0065 teardown despawns every net entity, so a parallel session's probe reads a
/// unit-less world and prints garbage; it happened, method.md records it).
///
/// Slot identity comes from the compiled-in manifest path, because that is what the pool guarantees
/// is unique per session: every slot has its own checkout and its own `target/`. Outside a pool slot
/// (the primary checkout, which is the director's) the guard is inert — it has no business having an
/// opinion about a login it cannot attribute. That is why this lives here and not in `preflight`
/// (decision 0649's home for it, until 1174 made `preflight` dev-only): gameplay's login policy
/// calls it, and gameplay may not call an instrument.
///
/// `WOW_ALLOW_ACCOUNT=1` is the escape hatch for the rare deliberate cross-account run; it turns the
/// refusal into a warning rather than silence, because the kick still happens.
pub(crate) fn account_guard(user: &str) -> Result<(), String> {
guard_for(compiled_slot(), user)
}
/// The pool slot this binary was *compiled* in, or `None`.
///
/// A player build is never in a pool slot, so 1174 read `env!("CARGO_MANIFEST_DIR")`
/// unconditionally on the grounds that the ladder collapses to `Ok(())` anyway. It does — but the
/// path is still a **string in the shipped binary**, naming the build machine's home directory,
/// which is the one thing decision 1175 exists to end (its falsifier is literally
/// `strings <player binary> | grep -c '/Users/…'`). It costs one `cfg` to not ship it, and it is
/// what makes that falsifier readable: after this, the only source-tree paths left in a player
/// binary are debuginfo and panic metadata, never data the program acts on.
#[cfg(feature = "dev")]
fn compiled_slot() -> Option<u32> {
pool_slot(env!("CARGO_MANIFEST_DIR"))
}
#[cfg(not(feature = "dev"))]
fn compiled_slot() -> Option<u32> {
None
}
/// [`account_guard`]'s decision, with the slot passed in so the ladder is testable from any
/// checkout (the real one reads a compile-time path that differs per worktree).
fn guard_for(slot: Option<u32>, user: &str) -> Result<(), String> {
let Some(slot) = slot else {
return Ok(());
};
let mine = format!("probe{slot}");
let user_lc = user.to_ascii_lowercase();
if user_lc == mine {
return Ok(());
}
let whose = if user_lc == "one" {
"the DIRECTOR's account — logging in on it kicks them out of their live session mid-play"
} else if user_lc.starts_with("probe") && user_lc[5..].chars().all(|c| c.is_ascii_digit()) {
"ANOTHER worktree slot's probe account — logging in on it kicks that session's probe out \
of the world, and its next sample reads a unit-less world"
} else {
return Ok(()); // a bystander account (`two`, a fresh test account): not ours to police
};
Err(format!(
"the env fast path is about to log in as `{user}` from pool-{slot}, and that is {whose}. \
This slot's identity is WOW_USER=probe{slot} WOW_PASS=pprobe{slot} \
WOW_CHAR=Probe{spelled} (method.md \"The local vmangos server\").",
spelled = spell_digit(slot)
))
}
/// This slot's index as the word the probe identity spells it with (`pool-4` → `"four"`), or `None`
/// outside a pool slot. The one place anything else should ask "which session am I?" — the rig keys
/// its derived character names off it, and the preflight banner names it.
pub(crate) fn slot_word() -> Option<&'static str> {
compiled_slot().map(spell_digit)
}
/// The pool slot index in a `…/benilla-wt/pool-<N>/…` manifest path; `None` for the primary
/// checkout and any non-pool worktree. Dev-only, because [`compiled_slot`] is its only caller and
/// a player build must not name a manifest path at all (1175).
#[cfg(any(feature = "dev", test))]
fn pool_slot(manifest_dir: &str) -> Option<u32> {
let mut parts = manifest_dir.split('/');
while let Some(part) = parts.next() {
if part == "benilla-wt" {
return parts.next()?.strip_prefix("pool-")?.parse().ok();
}
}
None
}
/// The probe character's name suffix for slot `n` — vmangos names carry no digits, so the pool
/// index is spelled (`pool-4` → `Probefour`).
fn spell_digit(n: u32) -> &'static str {
match n {
0 => "zero",
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
6 => "six",
7 => "seven",
8 => "eight",
9 => "nine",
_ => "<n>",
}
}
#[cfg(test)]
mod tests {
use super::*;
/// **The dev plane has exactly one door** (decision 1176).
///
/// The player-build gate (`cargo build -p benilla --no-default-features`) catches a *symbol*
/// crossing into a dev module. It cannot catch this: `benilla_world::modkeys::dev_chord` is
/// engine, always compiled, so a gameplay module that asks it for `Ctrl+Shift+F` compiles
/// perfectly in a player build and ships free-fly to a player. 1174 landed exactly that, green.
///
/// So the rule is checked instead of remembered, the way 0789 checks the probe clock: outside
/// the dev roots, `dev_chord` is [`super::dev_chord`]'s to call and nobody else's. A dev module
/// may call the engine's directly — it does not exist in a player build to be reached.
///
/// This does NOT police [`benilla_world::modkeys::DEV_CHORD`], the display string: naming the
/// plane in a label is not offering a key.
#[test]
fn the_dev_plane_has_exactly_one_door() {
// Everything that is compiled out by `--no-default-features`, plus this module, which is
// the door itself. Anything else asking the engine for a dev chord is the bug.
const DEV_ROOTS: &[&str] = &[
"capture/",
"debug_panel/",
"asset_churn.rs",
"dev.rs",
"hover_log.rs",
"perf/",
"preflight.rs",
"probe_shield.rs",
"lib.rs",
"run_mode.rs",
];
// Assembled at runtime so the checker does not flag its own source — the same trick, and
// the same proof of teeth, as `capture::probes`' clock test.
let needle = format!("modkeys::{}(", "dev_chord");
// The second clause: `#[cfg(feature = "dev")]` itself. Seam knowledge has exactly three
// addresses — this module (the always-present layer), `dev.rs` (the group), and `lib.rs`
// (the module declarations). A `cfg` attribute anywhere else means a gameplay module has
// learned the seam exists, which is the state 1174's whole diff cleared the tree out of,
// and which spreads one file at a time if nothing objects.
let cfg_needle = format!("feature = {}dev{}", '"', '"');
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders = Vec::new();
let mut stack = vec![src.clone()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src is readable") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
let rel = path
.strip_prefix(&src)
.expect("under src")
.to_string_lossy()
.replace('\\', "/");
if DEV_ROOTS.iter().any(|r| rel.starts_with(r)) {
continue;
}
let text = std::fs::read_to_string(&path).expect("source is readable");
for (n, line) in text.lines().enumerate() {
// Doc comments name the function constantly; only real calls matter.
let t = line.trim();
if t.starts_with("//") {
continue;
}
if t.contains(&needle) || t.contains(&cfg_needle) {
offenders.push(format!("{rel}:{} {t}", n + 1));
}
}
}
}
assert!(
offenders.is_empty(),
"a dev-chord affordance outside the dev roots must go through `run_mode::dev_chord`, \
which is `false` in a player build — the engine's `modkeys::dev_chord` is always \
compiled, so calling it directly ships a live dev key to a player and the build gate \
cannot see it (decision 1176). And a `#[cfg(feature = \"dev\")]` outside `run_mode`, \
`dev.rs` and `lib.rs` means a gameplay module has learned the seam exists — ask \
`run_mode::dev_affordances()` instead (decision 1179). Offenders:\n {}",
offenders.join("\n "),
);
}
/// **Two questions, two facts** (decision 1769) — and the fixture is the line the director
/// actually types, so a future merge of the two answers fails here rather than in their game.
#[test]
fn env_credentials_are_not_a_claim_about_who_is_in_the_room() {
use crate::local_state::test_env::{EnvGuard, ENV_LOCK};
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _decl = EnvGuard::unset("WOW_UNATTENDED");
let _cap = EnvGuard::unset("WOW_CAPTURE");
let _rig = EnvGuard::unset("WOW_RIG");
// A player build's defaults: nothing set, nothing assumed.
{
let _u = EnvGuard::unset("WOW_USER");
let _p = EnvGuard::unset("WOW_PASS");
let _c = EnvGuard::unset("WOW_CHAR");
assert!(!env_login() && !unattended());
}
// `WOW_USER=one WOW_PASS=pone cargo play` — `.cargo/config.toml`'s own example, and the
// director's launch line since 2026-08-29. Log them in without typing, yes. Decide things
// on their behalf, no.
let _u = EnvGuard::set("WOW_USER", "one");
let _p = EnvGuard::set("WOW_PASS", "pone");
assert!(env_login(), "the fast path still submits for them");
assert!(
!unattended(),
"credentials in the environment are not an empty chair",
);
// The declaration, and the two runs that are automated by construction.
for key in ["WOW_UNATTENDED", "WOW_CAPTURE", "WOW_RIG"] {
let _d = EnvGuard::set(key, "1");
assert!(unattended(), "{key} declares the run driverless");
}
assert!(!unattended(), "and each guard puts it back");
}
#[test]
fn the_guard_only_polices_accounts_that_belong_to_someone() {
// Our own slot's probe: the whole point of the identity.
assert!(guard_for(Some(4), "probe4").is_ok());
assert!(guard_for(Some(4), "PROBE4").is_ok()); // vmangos accounts are case-insensitive
// The director's account, and a neighbouring slot's probe: both kick a live session.
let director = guard_for(Some(4), "one").unwrap_err();
assert!(director.contains("DIRECTOR") && director.contains("WOW_USER=probe4"));
// The override hint belongs to the caller that can act on it, not to the reason.
assert!(!director.contains("WOW_ALLOW_ACCOUNT"));
assert!(guard_for(Some(4), "probe7")
.unwrap_err()
.contains("ANOTHER worktree slot"));
// A bystander account is nobody's to police, and outside a pool slot we have no standing.
assert!(guard_for(Some(4), "two").is_ok());
assert!(guard_for(None, "one").is_ok());
}
#[test]
fn the_probe_character_name_spells_the_slot() {
// vmangos player names carry no digits, so `Probe4` cannot exist — the pool index is spelled.
assert!(guard_for(Some(0), "one")
.unwrap_err()
.contains("WOW_CHAR=Probezero"));
assert!(guard_for(Some(9), "one")
.unwrap_err()
.contains("WOW_CHAR=Probenine"));
}
#[test]
fn the_slot_is_read_off_the_manifest_path() {
assert_eq!(
pool_slot("/Users/sam/dev/benilla-wt/pool-7/crates/benilla"),
Some(7)
);
assert_eq!(pool_slot("/Users/sam/dev/benilla-wow/crates/benilla"), None);
assert_eq!(
pool_slot("/Users/sam/dev/benilla-wow/.claude/worktrees/x/crates/benilla"),
None
);
}
}