Skip to main content

cloud/
validate.rs

1//! Workspace-wide lint checks for the `.yah/` declaration tree (R470-T3).
2//!
3//! Every check shares one shape — walk the declarations, return a list of
4//! findings, empty means clean:
5//!
6//! - **alias collision** ([`check_alias_collisions`]) — alias names declared
7//!   in any service component's `[aliases]` block must be workspace-globally
8//!   unique. Two services declaring the same alias is a consumer-site
9//!   ambiguity (`yah-app.toml` says `alias = "whisper-default-ggml"` — which
10//!   catalog wins?).
11//! - **port collision** ([`check_port_collisions`]) — two local-tier mirror
12//!   slots binding the same localhost port (R602-B4).
13//! - **inert taint** ([`check_inert_taints`]) — a `taints` entry in
14//!   `.yah/infra/machines/*.toml` that no scheduler path can read
15//!   (W305/R742-T4).
16//! - **retired arch tag** ([`check_retired_arch_tags`]) — a machine still
17//!   carrying the `tier:<arch>` build-worker mesh tag R763 renamed.
18//! - **unroled sovereign member**
19//!   ([`check_unroled_sovereign_members`]) — a machine naming a
20//!   `sovereign_group` without saying whether it votes in it (R605-F12).
21//! - **LAN dial target** ([`check_lan_dial_targets`]) — a machine whose
22//!   `[connect].yubaba` is an RFC1918 literal, i.e. a break-glass address
23//!   sitting in the field every automated path dials (R605-T10).
24//! - **ingress floating IP** ([`check_ingress_floating_ip`]) — a machine whose
25//!   `ingress_floating_ip` no adapter can move, or two machines in one
26//!   `sovereign_group` naming *different* ingress IPs (R859-F2).
27//! - **ingress collation** ([`collate_workspace_ingress`]) — two services
28//!   whose declared edges cannot share the node they both front through
29//!   (W305/R742-F2). The only check here that is *inherently* cross-service:
30//!   each service's own `yah cloud apply` sees one mirror, so a hostname
31//!   claimed twice on one box is invisible from either side of it.
32//!
33//! What unites them: each catches a declaration that *parses*, so nothing
34//! downstream complains, but which means something other than what it reads
35//! as. That is the class of bug this module exists for — a hard error is the
36//! type system's job, and a wrong-but-valid declaration is nobody's until
37//! someone writes the lint.
38//!
39//! Invoked by `yah cloud validate` and as a preflight in `yah cloud apply`.
40//!
41//! @yah:relay(R787, "Consolidate the four .yah/infra/machines/*.toml walks in oss/yubaba/crates/cloud/src/validate.rs behind one loader")
42//! @yah:status(review)
43//! @yah:at(2026-08-20T05:26:41Z)
44//! @yah:assignee(agent:bundle-anthropic-miravel)
45//! @yah:notify_on(R555, "Re-run `cd oss/yubaba && cargo test -p yah-cloud --lib validate::` — it was blocked crate-wide by R555's in-flight AdmissionGrant.secrets field missing from crates/cloud/src/reconciler/lowering_golden.rs's TransformRecipe fixture (E0063), unrelated to R787's validate.rs change. Confirm the two new tests (tolerant_mode_skips_an_unparseable_toml_and_still_pairs_the_path, strict_mode_fails_the_whole_load_on_one_unparseable_toml) and the existing validate:: suite pass once that's fixed.")
46//! @yah:handoff("Consolidated the four .yah/infra/machines/*.toml walks (check_inert_taints, check_retired_arch_tags, check_unroled_sovereign_members, load_machines) behind one shared load_machine_tomls(workspace_root, mode) in oss/yubaba/crates/cloud/src/validate.rs. Returns Vec<(PathBuf, MachineConfig)> per the agreed shape (Ashguard/R605-F12) so lint findings still name the file.")
47//! @yah:handoff("MachineLoadMode::Tolerant preserves the three lints' existing skip-and-warn behavior; MachineLoadMode::Strict preserves load_machines' hard-fail behavior for collate_workspace_ingress's placement resolution (R772) -- no behavior change at any of the four call sites.")
48//! @yah:handoff("Closed the vacuous-pass trap flagged in the ticket: added assert_machine_toml_parses(), called by write_machine, write_machine_tags, and write_sovereign_machine right after writing each fixture, so a future edit that drops a required MachineConfig field fails loudly at the helper instead of being silently skipped by the tolerant loader and producing a vacuous assert-empty pass.")
49//! @yah:handoff("Added two new tests locking in the Strict/Tolerant contract directly: tolerant_mode_skips_an_unparseable_toml_and_still_pairs_the_path, strict_mode_fails_the_whole_load_on_one_unparseable_toml.")
50//! @yah:handoff("R772's load_machines and R605-F12's check_unroled_sovereign_members were already landed and committed on this file before this pass (verified via git diff being empty and no live session on validate.rs at pickup) -- both were settled, so this was safe to do now rather than wait further.")
51//! @yah:verify("cargo check -p yah-cloud --lib (from repo root) -- clean, 0 warnings in validate.rs (2 pre-existing unrelated warnings elsewhere: mesofact_static.rs unused imports, and one more in a different file).")
52//! @yah:verify("cargo test -p yah-cloud --lib validate:: (from oss/yubaba, required for dev-deps) -- BLOCKED, not failing: E0063 missing field `secrets` in crates/cloud/src/reconciler/lowering_golden.rs's TransformRecipe fixture, caused by R555's in-flight uncommitted AdmissionGrant.secrets field in oss/qed/crates/velveteen-exec (git status confirms those files are live-modified, lowering_golden.rs is not). This is the same blocker R605-F12's own verify section recorded. Filed @yah:notify_on(R555) on this ticket so whoever reopens it re-runs the suite once R555 lands.")
53//! @yah:verify("Manual read-through of the diff: every lint's iteration body (finding construction) is byte-identical to before, only the walk+parse boilerplate was extracted -- no logic changed at any of the four call sites.")
54//! @yah:gotcha("Test verification for this crate is blocked camp-wide right now by R555 (live, Ashguard) -- see notify_on. Not this ticket's bug; do not attempt to fix lowering_golden.rs or velveteen-exec from here.")
55
56use std::collections::BTreeMap;
57use std::path::{Path, PathBuf};
58
59use anyhow::Context as _;
60
61use crate::config::{
62    live_taint_keys, private_ipv4_from_url, MachineConfig, MirrorConfig, MirrorProviderSlot,
63    Provider, ServiceConfig,
64};
65use crate::paths::{machines_dir, services_dir};
66
67/// Where an alias is declared — points the operator at the source row.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct AliasSource {
70    /// Service name (matches `service.toml`'s `name` field).
71    pub service: String,
72    /// Component `id` within that service.
73    pub component_id: String,
74    /// Absolute path to the `workload.toml` containing the `[aliases]` block.
75    pub workload_toml: PathBuf,
76}
77
78/// A duplicate alias declaration found across two components.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct AliasCollision {
81    pub alias: String,
82    pub first: AliasSource,
83    pub second: AliasSource,
84}
85
86impl AliasCollision {
87    /// Human-readable error message matching the format described in W193.
88    pub fn message(&self) -> String {
89        format!(
90            "alias {:?} declared in both {} (component {}) and {} (component {})\n\
91             \u{2192} rename the alias in one of these files:\n  {}\n  {}",
92            self.alias,
93            self.first.service,
94            self.first.component_id,
95            self.second.service,
96            self.second.component_id,
97            self.first.workload_toml.display(),
98            self.second.workload_toml.display(),
99        )
100    }
101}
102
103/// Walk every service's static-asset components and collect all `(alias →
104/// source)` mappings. Returns a list of collisions (empty when clean).
105///
106/// Missing `.yah/services/` directory is not an error — returns empty.
107pub fn check_alias_collisions(workspace_root: &Path) -> anyhow::Result<Vec<AliasCollision>> {
108    let dir = services_dir(workspace_root);
109    if !dir.exists() {
110        return Ok(vec![]);
111    }
112
113    // alias_name → first source seen
114    let mut seen: BTreeMap<String, AliasSource> = BTreeMap::new();
115    let mut collisions = Vec::new();
116
117    let mut entries: Vec<_> = std::fs::read_dir(&dir)
118        .with_context(|| format!("reading {}", dir.display()))?
119        .filter_map(|e| e.ok())
120        .filter(|e| e.path().is_dir())
121        .collect();
122    entries.sort_by_key(|e| e.file_name());
123
124    for entry in entries {
125        let svc_dir = entry.path();
126        let service_toml = svc_dir.join("service.toml");
127        if !service_toml.exists() {
128            continue;
129        }
130        let service = match ServiceConfig::load(&service_toml) {
131            Ok(s) => s,
132            Err(e) => {
133                tracing::warn!(
134                    path = %service_toml.display(),
135                    error = %e,
136                    "skipping service with unparseable service.toml"
137                );
138                continue;
139            }
140        };
141
142        for component in &service.components {
143            if component.kind != "static-asset" {
144                continue;
145            }
146            let workload_dir = workspace_root.join(&component.path);
147            let workload_toml_path = workload_dir.join("workload.toml");
148            if !workload_toml_path.exists() {
149                continue;
150            }
151
152            let aliases = match load_static_asset_aliases(&workload_toml_path) {
153                Ok(a) => a,
154                Err(e) => {
155                    tracing::warn!(
156                        path = %workload_toml_path.display(),
157                        error = %e,
158                        "skipping workload.toml with parse error"
159                    );
160                    continue;
161                }
162            };
163
164            for alias_name in aliases.keys() {
165                let source = AliasSource {
166                    service: service.name.clone(),
167                    component_id: component.id.clone(),
168                    workload_toml: workload_toml_path.clone(),
169                };
170                if let Some(first) = seen.get(alias_name) {
171                    collisions.push(AliasCollision {
172                        alias: alias_name.clone(),
173                        first: first.clone(),
174                        second: source,
175                    });
176                } else {
177                    seen.insert(alias_name.clone(), source);
178                }
179            }
180        }
181    }
182
183    Ok(collisions)
184}
185
186/// Load the `[aliases]` block from a `workload.toml` that must be a
187/// `static-asset` kind. Returns an empty map for non-static-asset workloads
188/// (so the caller skips them silently).
189fn load_static_asset_aliases(path: &Path) -> anyhow::Result<BTreeMap<String, String>> {
190    let src =
191        std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
192    let workload: workload_spec::Workload =
193        toml::from_str(&src).with_context(|| format!("parsing {}", path.display()))?;
194    match workload {
195        workload_spec::Workload::StaticAsset(w) => Ok(w.aliases),
196        _ => Ok(BTreeMap::new()),
197    }
198}
199
200// ── Port collisions (R602-B4) ────────────────────────────────────────────────
201
202/// Where a host port is declared — points the operator at the (service, env,
203/// slot) that binds it.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct PortSource {
206    /// Service name (matches `service.toml`'s `name` field).
207    pub service: String,
208    /// Environment (file stem of `mirrors/<env>.toml`).
209    pub env: String,
210    /// Provider slot role the port sits under (`providers.<role>`).
211    pub slot_role: String,
212    /// The field that carried the port (`port` / `api_port` / `console_port`).
213    pub field: String,
214}
215
216/// Two local-tier mirror slots that bind the same host port. Because local
217/// mirrors share the operator's localhost, both binding the same port collide
218/// when brought up together — and the local-static adopt probe (a bare TCP
219/// connect) may then silently adopt the *wrong* service (R602-B4: `scrabcake`
220/// dev and `yah-marketing` pond both on 4322).
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct PortCollision {
223    pub port: u16,
224    pub first: PortSource,
225    pub second: PortSource,
226}
227
228impl PortCollision {
229    /// True when the two binders belong to different services — the dangerous
230    /// case, because the local-static adopt probe can then silently adopt the
231    /// *other* service's server. Same-service reuse (e.g. one service's dev +
232    /// cloud mirrors sharing a port) is only a can't-co-run bind conflict.
233    pub fn is_cross_service(&self) -> bool {
234        self.first.service != self.second.service
235    }
236
237    /// Human-readable error naming both binders + the fix.
238    pub fn message(&self) -> String {
239        format!(
240            "host port {} is bound by both {}/{} (providers.{}.{}) and {}/{} (providers.{}.{})\n\
241             \u{2192} give one a distinct port — local mirrors share the operator's localhost, so \
242             two slots on the same port collide, and the local-static adopt probe may silently \
243             adopt the wrong service.",
244            self.port,
245            self.first.service,
246            self.first.env,
247            self.first.slot_role,
248            self.first.field,
249            self.second.service,
250            self.second.env,
251            self.second.slot_role,
252            self.second.field,
253        )
254    }
255}
256
257/// Host-port field names a local-binding mirror slot may declare.
258const PORT_FIELDS: &[&str] = &["port", "api_port", "console_port"];
259
260/// True when this slot binds a port on the operator's localhost, so its port
261/// contends with every other local slot. Reference slots (`use = "..."`) and
262/// cloud/CF slots don't bind localhost and are skipped.
263fn slot_binds_localhost(slot: &MirrorProviderSlot) -> bool {
264    matches!(
265        slot.inline_kind(),
266        Some(Provider::LocalStatic | Provider::MiniflareContainer | Provider::MinioContainer)
267    )
268}
269
270/// Walk every service mirror and flag host-port reuse across local-tier
271/// provider slots (R602-B4). Only slots that bind a port on the operator's
272/// localhost are considered (`local-static`, `miniflare-container`,
273/// `minio-container`) — cloud/CF slots don't contend for localhost.
274///
275/// Deterministic: services + mirror envs are walked in sorted order, slot
276/// roles sorted, `PORT_FIELDS` in declared order — so the "first" binder of a
277/// port is stable across runs. Missing `.yah/services/` is not an error.
278pub fn check_port_collisions(workspace_root: &Path) -> anyhow::Result<Vec<PortCollision>> {
279    // port → first source seen
280    let mut seen: BTreeMap<u16, PortSource> = BTreeMap::new();
281    let mut collisions = Vec::new();
282
283    for m in load_service_mirrors(workspace_root)? {
284        let mut roles: Vec<&String> = m.mirror.providers.keys().collect();
285        roles.sort();
286        for role in roles {
287            let slot = &m.mirror.providers[role];
288            if !slot_binds_localhost(slot) {
289                continue;
290            }
291            for field in PORT_FIELDS {
292                let Some(port) = crate::reconciler::slot_field_u16(slot.fields(), field) else {
293                    continue;
294                };
295                let source = PortSource {
296                    service: m.service.clone(),
297                    env: m.env.clone(),
298                    slot_role: role.clone(),
299                    field: (*field).to_string(),
300                };
301                match seen.get(&port) {
302                    Some(first) => collisions.push(PortCollision {
303                        port,
304                        first: first.clone(),
305                        second: source,
306                    }),
307                    None => {
308                        seen.insert(port, source);
309                    }
310                }
311            }
312        }
313    }
314
315    Ok(collisions)
316}
317
318// ── Shared machine-TOML loader (R787) ───────────────────────────────────────
319
320/// Every `.yah/infra/machines/*.toml` **this camp itself declares**, paired
321/// with the path that declared it — every lint finding names the file the
322/// operator opens.
323///
324/// Camp-local by construction, and that is the whole of its contract: a lint
325/// exists to tell an operator about a file they can edit, and a borrowed
326/// machine is declared in another camp's tree where they cannot. A lint that
327/// cannot be resolved is noise that trains the operator to ignore the check.
328///
329/// **This is not the camp's fleet inventory.** For any caller that resolves a
330/// machine *name* to a machine — placement, ingress collation, the sovereign
331/// apex render — use [`crate::config::resolve_fleet_inventory`], which
332/// additionally overlays every fleet borrowed through
333/// `.yah/infra/sources.toml`. R870-B13: this function used to carry a
334/// `MachineLoadMode::Strict` arm and serve both jobs, which made a borrowing
335/// camp (empty local `machines/`, one `[[source]]` link) resolve against an
336/// empty fleet and fail its apex render on a machine that was declared all
337/// along, one directory over.
338///
339/// Missing `.yah/infra/machines/` is not an error — a fresh camp, and every
340/// borrowing camp, declares no nodes of its own. An unreadable or unparseable
341/// file is skipped with a `tracing::warn!` rather than failing the sweep: a
342/// peer's half-written scaffold on a shared tree must not blind the sweep to
343/// every other finding it would report. Files are walked in sorted-filename
344/// order so findings are deterministic across runs.
345pub fn load_camp_local_machine_tomls(
346    workspace_root: &Path,
347) -> anyhow::Result<Vec<(PathBuf, MachineConfig)>> {
348    let dir = machines_dir(workspace_root);
349    if !dir.exists() {
350        return Ok(Vec::new());
351    }
352
353    let mut entries: Vec<_> = std::fs::read_dir(&dir)
354        .with_context(|| format!("reading {}", dir.display()))?
355        .filter_map(|e| e.ok())
356        .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
357        .collect();
358    entries.sort_by_key(|e| e.file_name());
359
360    let mut out = Vec::with_capacity(entries.len());
361    for entry in entries {
362        let path = entry.path();
363        let src = match std::fs::read_to_string(&path) {
364            Ok(s) => s,
365            Err(e) => {
366                tracing::warn!(path = %path.display(), error = %e, "skipping unreadable machine toml");
367                continue;
368            }
369        };
370        let machine: MachineConfig = match toml::from_str(&src) {
371            Ok(m) => m,
372            Err(e) => {
373                tracing::warn!(path = %path.display(), error = %e, "skipping unparseable machine toml");
374                continue;
375            }
376        };
377        out.push((path, machine));
378    }
379    Ok(out)
380}
381
382// ── R742-T4 (W305): inert node taints ──────────────────────────────────────
383
384/// A declared node taint no placement decision can read.
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct InertTaint {
387    /// Machine name as declared in the TOML.
388    pub machine: String,
389    /// Path to the declaring `.yah/infra/machines/<name>.toml`.
390    pub machine_toml: PathBuf,
391    /// The offending key.
392    pub key: String,
393}
394
395impl InertTaint {
396    pub fn message(&self) -> String {
397        format!(
398            "machine {:?} declares the taint {:?}, which no placement decision can read\n\
399             \u{2192} a taint fires in exactly two ways: as repulsion \
400             (`no-server` / `no-appliance` / `no-job`, an absolute block on that archetype), \
401             or as affinity (a key a workload names in `yah.placement.requires-taint`).\n\
402             \u{2192} legal keys today: {}\n\
403             \u{2192} if this is a *fact* about the node rather than a placement input, \
404             move it to `mesh_tags` or a comment; if it should really constrain placement, \
405             add it to cloud::config::AFFINITY_TAINT_KEYS together with the workload that \
406             requires it.\n  {}",
407            self.machine,
408            self.key,
409            live_taint_keys().join(", "),
410            self.machine_toml.display(),
411        )
412    }
413}
414
415/// Flag every taint in `.yah/infra/machines/*.toml` that the scheduler cannot
416/// act on (W305 finding 1 / R742-T4).
417///
418/// Why this is a *lint* and not a parse error: an inert taint breaks nothing.
419/// It changes no placement decision — that is the entire complaint. Refusing
420/// to deserialize would make an unrelated `yah cloud machine status` fail on a
421/// cosmetic problem, so the judgement is made where the operator asks for it.
422///
423/// **Camp-local machines only.** [`machines_dir`] is deliberately not the
424/// merged view `CloudConfig::load` builds from `.yah/infra/sources.toml` — a
425/// borrowed machine is declared in someone else's tree, where this camp cannot
426/// fix it, and a lint that cannot be resolved is noise that trains the
427/// operator to ignore the whole check.
428///
429/// Missing `.yah/infra/machines/` is not an error — a fresh camp declares no
430/// nodes. Unparseable files are skipped with a warning rather than aborting
431/// the sweep, matching [`check_port_collisions`]; whatever is wrong with them
432/// is a bigger problem than a taint key and surfaces on the load path.
433pub fn check_inert_taints(workspace_root: &Path) -> anyhow::Result<Vec<InertTaint>> {
434    let mut found = Vec::new();
435    for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
436        for key in machine.inert_taints() {
437            found.push(InertTaint {
438                machine: machine.name.clone(),
439                machine_toml: path.clone(),
440                key: key.to_string(),
441            });
442        }
443    }
444    Ok(found)
445}
446
447// ── R763 (W314): the retired `tier:` arch mesh tag ─────────────────────────
448
449/// The mesh-tag prefix that used to carry a build-worker's CPU architecture.
450/// Retired 2026-08-14 — the value was an architecture, not a tier, and it was
451/// squatting a prefix the environment axis wants.
452const RETIRED_ARCH_TAG_PREFIX: &str = "tier:";
453/// What it became. [`qed::platform::build_worker_mesh_tags`] emits this.
454const ARCH_TAG_PREFIX: &str = "arch:";
455
456/// A machine still declaring the retired `tier:<arch>` build-worker mesh tag.
457#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct RetiredArchTag {
459    pub machine: String,
460    pub machine_toml: PathBuf,
461    /// The offending tag, verbatim (e.g. `tier:x86`).
462    pub tag: String,
463}
464
465impl RetiredArchTag {
466    /// The replacement tag — same value, current prefix.
467    pub fn replacement(&self) -> String {
468        format!(
469            "{ARCH_TAG_PREFIX}{}",
470            self.tag.trim_start_matches(RETIRED_ARCH_TAG_PREFIX)
471        )
472    }
473
474    pub fn message(&self) -> String {
475        format!(
476            "machine {:?} declares the retired mesh tag {:?} — rename it to {:?}\n\
477             \u{2192} `tier:x86` / `tier:arm` were renamed to `arch:x86` / `arch:arm` \
478             (R763): the value is a CPU architecture, not a tier, and `tier:` is \
479             reserved for the environment axis.\n\
480             \u{2192} this does not fail loudly on its own, which is why it is checked \
481             here: `qed::platform::build_worker_mesh_tags` now requests `arch:<arch>`, \
482             and placement is SUPERSET matching, so a node still carrying the old tag \
483             simply stops matching and the build reports 'no node' instead of \
484             'wrong tag'.\n  {}",
485            self.machine,
486            self.tag,
487            self.replacement(),
488            self.machine_toml.display(),
489        )
490    }
491}
492
493/// Flag every machine still carrying a `tier:<arch>` build-worker mesh tag.
494///
495/// Same lint family, and the same camp-local-only scoping, as
496/// [`check_inert_taints`] — but note the failure it catches is *quieter* than
497/// an inert taint. An inert taint changes no decision; a stale arch tag changes
498/// the decision to "no candidate node", and the operator sees a placement
499/// failure with no hint that a tag rename caused it.
500pub fn check_retired_arch_tags(workspace_root: &Path) -> anyhow::Result<Vec<RetiredArchTag>> {
501    let mut found = Vec::new();
502    for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
503        for tag in machine
504            .mesh_tags
505            .iter()
506            .filter(|t| t.starts_with(RETIRED_ARCH_TAG_PREFIX))
507        {
508            found.push(RetiredArchTag {
509                machine: machine.name.clone(),
510                machine_toml: path.clone(),
511                tag: tag.clone(),
512            });
513        }
514    }
515    Ok(found)
516}
517
518// ── R605-F12: a group stamp with no role beside it ─────────────────────────
519
520/// A machine declaring `sovereign_group` without an explicit `sovereign_role`.
521#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct UnroledSovereignMember {
523    pub machine: String,
524    pub machine_toml: PathBuf,
525    /// The group it declares — named in the message because the answer to
526    /// "voter or not" depends on which blast radius is being joined.
527    pub group: String,
528}
529
530impl UnroledSovereignMember {
531    pub fn message(&self) -> String {
532        format!(
533            "machine {:?} declares `sovereign_group = {:?}` but no `sovereign_role`\n\
534             \u{2192} membership and quorum eligibility are separate axes (R605-F12): a node can \
535             be inside a group's blast radius — its secrets, its upgrade cadence, its \
536             destruction — and still never hold a seat in its quorum.\n\
537             \u{2192} an absent role reads as `\"voter\"`, so this box is quorum-eligible today. \
538             That is the pre-R605-F12 meaning and is often right; the complaint is that nothing \
539             records whether anyone decided it.\n\
540             \u{2192} add `sovereign_role = \"voter\"` or `sovereign_role = \"non-voter\"` as a \
541             TOP-LEVEL key (below a `[table]` header TOML makes it a field of that table, and \
542             every consumer reads it as absent).\n  {}",
543            self.machine,
544            self.group,
545            self.machine_toml.display(),
546        )
547    }
548}
549
550/// Flag every machine that names a sovereign group without saying whether it
551/// votes in it (R605-F12).
552///
553/// Same lint family and the same camp-local-only scoping as
554/// [`check_inert_taints`], for a failure one step quieter than either of the
555/// others: an inert taint changes no decision and a stale arch tag changes it
556/// to "no candidate node", but an unwritten role changes nothing *visible* and
557/// silently grants a quorum seat. That is exactly the shape this ticket was
558/// opened about — a guarantee resting on which fields happen to be absent — so
559/// closing it by making the *other* absence load-bearing would have been the
560/// same bug with the sign flipped.
561///
562/// Why a lint rather than a required field: [`MachineConfig`] is deserialized
563/// from foreign trees too (`.yah/infra/sources.toml` overlays another camp's
564/// machines, which may predate this field entirely), and a hard parse error
565/// there is unfixable from here. The judgement is made where the operator asks
566/// for it, against machines this camp can actually edit.
567pub fn check_unroled_sovereign_members(
568    workspace_root: &Path,
569) -> anyhow::Result<Vec<UnroledSovereignMember>> {
570    let mut found = Vec::new();
571    for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
572        if let (Some(group), None) = (&machine.sovereign_group, &machine.sovereign_role) {
573            found.push(UnroledSovereignMember {
574                machine: machine.name.clone(),
575                machine_toml: path.clone(),
576                group: group.clone(),
577            });
578        }
579    }
580    Ok(found)
581}
582
583// ── R605-T10: a LAN literal in the field automation dials ──────────────────
584
585/// A machine whose `[connect].yubaba` points at an RFC1918 private address.
586#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct LanDialTarget {
588    pub machine: String,
589    pub machine_toml: PathBuf,
590    /// The offending URL, verbatim (e.g. `http://192.168.10.11:7443`).
591    pub url: String,
592    /// The registered mesh address, when the box has one — the difference
593    /// between "delete a line" and "go join the mesh first".
594    pub mesh_ipv4: Option<String>,
595}
596
597impl LanDialTarget {
598    pub fn message(&self) -> String {
599        let fix = match &self.mesh_ipv4 {
600            Some(ip) => format!(
601                "\u{2192} this box IS mesh-joined at {ip}: DELETE the `yubaba` line. \
602                 `[registration].mesh_ipv4` composes with `[connect].yubaba_port` on its own, \
603                 and `[connect].address` already records the LAN address as metadata."
604            ),
605            None => "\u{2192} this box has no `[registration].mesh_ipv4`: mesh-join it and record \
606                     the tailnet address, or taint it out of placement. Until then it is \
607                     unresolvable to automation and `MachineConfig::reach` refuses it by name."
608                .to_string(),
609        };
610        format!(
611            "machine {:?} declares `[connect].yubaba = {:?}` — a LAN address in the field every \
612             automated path dials\n\
613             \u{2192} the LAN address is an emergency break-glass route, never an official one \
614             (R605-T10, operator 2026-08-19). Automation ALWAYS assumes the caller is not on that \
615             LAN, and this camp genuinely is not — it sits on 192.168.22.0/22 with no route to \
616             192.168.10.0/24.\n\
617             \u{2192} it does not sit beside the mesh route, it OVERRODE it: before this check, a \
618             declared literal beat `[registration].mesh_ipv4` outright (R707-T6), so a healthy \
619             mesh-joined build worker was elected and then dialed at an address only its own \
620             building can reach.\n\
621             {fix}\n\
622             \u{2192} keeping the LAN address is fine and wanted — in `[connect].address` and \
623             `[connect].ssh`, which no resolver dials. A manual SSH session may use it once a \
624             human confirms they are on that LAN.\n  {}",
625            self.machine,
626            self.url,
627            self.machine_toml.display(),
628        )
629    }
630}
631
632/// Flag every machine that has put a LAN literal in `[connect].yubaba`.
633///
634/// Same lint family and camp-local-only scoping as [`check_inert_taints`]. The
635/// failure this one catches is the loudest of the four and still went unnoticed
636/// for weeks, which is the argument for checking it: the declaration parses, the
637/// node is elected by placement, and the only symptom is a connect timeout at
638/// dial time attributed to the box rather than to its file.
639///
640/// Loopback is deliberately not flagged — `http://127.0.0.1:7443` is the
641/// pre-mesh "reach me through the SSH tunnel" declaration, a genuine statement
642/// that `hub::coordinator::is_loopback_url` already judges downstream.
643///
644/// A finding here is now belt-and-braces rather than the only guard:
645/// [`MachineConfig::reach`] refuses to dial a private literal regardless. The
646/// lint exists so the operator hears about it from the file rather than from a
647/// roll that silently picked a different address than the one written down.
648pub fn check_lan_dial_targets(workspace_root: &Path) -> anyhow::Result<Vec<LanDialTarget>> {
649    let mut found = Vec::new();
650    for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
651        let Some(url) = machine.connect.as_ref().and_then(|c| c.yubaba.as_deref()) else {
652            continue;
653        };
654        if private_ipv4_from_url(url).is_none() {
655            continue;
656        }
657        found.push(LanDialTarget {
658            machine: machine.name.clone(),
659            machine_toml: path.clone(),
660            url: url.to_string(),
661            mesh_ipv4: machine.mesh_ipv4().map(str::to_string),
662        });
663    }
664    Ok(found)
665}
666
667// ── R859-F2: the ingress floating IP declaration ───────────────────────────
668
669/// A machine whose [`MachineConfig::ingress_floating_ip`] cannot mean what it
670/// says.
671#[derive(Debug, Clone, PartialEq, Eq)]
672pub struct IngressFloatingIpProblem {
673    pub machine: String,
674    pub machine_toml: PathBuf,
675    pub kind: IngressFloatingIpProblemKind,
676}
677
678/// The three ways an `ingress_floating_ip` declaration can be wrong.
679#[derive(Debug, Clone, PartialEq, Eq)]
680pub enum IngressFloatingIpProblemKind {
681    /// Present but blank — a key that reads as a declaration and is not one.
682    Blank,
683    /// The machine's `provider` has no floating-IP adapter, so nothing in this
684    /// codebase could ever move that IP.
685    NoAdapter { provider: String },
686    /// Another machine in the same `sovereign_group` names a *different*
687    /// ingress IP.
688    CohortDisagreement {
689        group: String,
690        this_ip: String,
691        other_machine: String,
692        other_ip: String,
693    },
694}
695
696impl IngressFloatingIpProblem {
697    pub fn message(&self) -> String {
698        let body = match &self.kind {
699            IngressFloatingIpProblemKind::Blank => {
700                "declares an empty `ingress_floating_ip`\n\
701                 \u{2192} an empty string is not \"no floating IP\" — omit the key entirely for \
702                 that, which is the normal case and a clean skip. A blank value reads as a \
703                 declaration and resolves to nothing."
704                    .to_string()
705            }
706            IngressFloatingIpProblemKind::NoAdapter { provider } => format!(
707                "declares an `ingress_floating_ip` but its provider is {provider:?}, which has \
708                 no floating-IP adapter\n\
709                 \u{2192} floating/reserved IPs are implemented for hetzner, ovh and vultr \
710                 (cloud::provider::floating_ip's registry). Nothing in this codebase can move \
711                 this IP, so the declaration is inert — and inert in the worst way, because it \
712                 reads as a working failover path.\n\
713                 \u{2192} if the box really does carry a public IP that never moves, that is \
714                 what the `public-ip` taint plus `[connect].address` already say."
715            ),
716            IngressFloatingIpProblemKind::CohortDisagreement {
717                group,
718                this_ip,
719                other_machine,
720                other_ip,
721            } => format!(
722                "declares `ingress_floating_ip = {this_ip:?}` but {other_machine} in the same \
723                 sovereign_group {group:?} declares {other_ip:?}\n\
724                 \u{2192} the ingress floating IP is ONE resource that moves between the boxes \
725                 of a cohort as ownership flips. Two ids means an ownership flip reassigns a \
726                 different IP than the one currently serving traffic — the old IP stays pointed \
727                 at the dead box and the new one was never in DNS.\n\
728                 \u{2192} the symptom is a failover that reports success and serves nothing, \
729                 which is why this is refused here rather than discovered during one."
730            ),
731        };
732        format!(
733            "machine {:?} {body}\n  {}",
734            self.machine,
735            self.machine_toml.display(),
736        )
737    }
738}
739
740/// Flag every `ingress_floating_ip` declaration that cannot do what it claims
741/// (R859-F2).
742///
743/// Same lint family and camp-local-only scoping as [`check_inert_taints`], for
744/// a failure that is quiet in the ordinary way and loud exactly once: nothing
745/// reads the field until public ingress moves, so a wrong declaration sits
746/// green for months and then fails during the one event it exists for.
747///
748/// **Absence is never a finding.** Most machines have no floating IP — mesh-only
749/// nodes, tunnel-fronted boxes, every provider without an adapter — and that is
750/// the normal shape, not an omission. Only a *present* declaration is judged.
751///
752/// Machines with no `sovereign_group` are exempt from the cohort check: with no
753/// group there is no cohort to disagree with, and two standalone boxes that
754/// happen to hold different IPs are two independent facts rather than one
755/// contradiction.
756pub fn check_ingress_floating_ip(
757    workspace_root: &Path,
758) -> anyhow::Result<Vec<IngressFloatingIpProblem>> {
759    let machines = load_camp_local_machine_tomls(workspace_root)?;
760    let mut found = Vec::new();
761
762    // First declaration seen per group, in load order — the one every later
763    // member of that group is compared against, so a cohort of N disagreeing
764    // machines yields N-1 findings pointing at one anchor rather than N^2
765    // pairwise ones.
766    let mut anchor: BTreeMap<String, (String, String)> = BTreeMap::new();
767
768    for (path, machine) in &machines {
769        let Some(ip) = machine.ingress_floating_ip.as_deref() else {
770            continue;
771        };
772        let mut push = |kind| {
773            found.push(IngressFloatingIpProblem {
774                machine: machine.name.clone(),
775                machine_toml: path.clone(),
776                kind,
777            })
778        };
779        if ip.trim().is_empty() {
780            push(IngressFloatingIpProblemKind::Blank);
781            continue;
782        }
783        if !crate::provider::provider_has_floating_ip_adapter(&machine.provider) {
784            push(IngressFloatingIpProblemKind::NoAdapter {
785                provider: machine.provider.clone(),
786            });
787        }
788        let Some(group) = machine.sovereign_group.as_deref() else {
789            continue;
790        };
791        match anchor.get(group) {
792            None => {
793                anchor.insert(group.to_string(), (machine.name.clone(), ip.to_string()));
794            }
795            Some((other_machine, other_ip)) if other_ip != ip => {
796                push(IngressFloatingIpProblemKind::CohortDisagreement {
797                    group: group.to_string(),
798                    this_ip: ip.to_string(),
799                    other_machine: other_machine.clone(),
800                    other_ip: other_ip.clone(),
801                });
802            }
803            Some(_) => {}
804        }
805    }
806    Ok(found)
807}
808
809// ── R742-F2 (W305): ingress edge collation ─────────────────────────────────
810
811/// One mirror, loaded with the identity every finding has to be able to name.
812#[derive(Debug, Clone)]
813pub struct LoadedMirror {
814    /// Service name (matches `service.toml`'s `name` field).
815    pub service: String,
816    /// Environment (file stem of `mirrors/<env>.toml`).
817    pub env: String,
818    /// Path to the mirror TOML — what an operator opens to fix a finding.
819    pub path: PathBuf,
820    pub mirror: MirrorConfig,
821}
822
823/// Every `.yah/services/<svc>/mirrors/<env>.toml` in the workspace, in a
824/// deterministic order (services sorted, then envs).
825///
826/// Shared by every cross-mirror check, because "walk the mirrors" is the one
827/// part all of them agree on and three hand-rolled copies of it drift. A
828/// service with no `service.toml`, or a mirror that will not parse, is skipped
829/// with a warning rather than aborting the sweep — whatever is wrong with it is
830/// a bigger problem than the lint and surfaces on the load path.
831pub fn load_service_mirrors(workspace_root: &Path) -> anyhow::Result<Vec<LoadedMirror>> {
832    let dir = services_dir(workspace_root);
833    if !dir.exists() {
834        return Ok(vec![]);
835    }
836
837    let mut svc_entries: Vec<_> = std::fs::read_dir(&dir)
838        .with_context(|| format!("reading {}", dir.display()))?
839        .filter_map(|e| e.ok())
840        .filter(|e| e.path().is_dir())
841        .collect();
842    svc_entries.sort_by_key(|e| e.file_name());
843
844    let mut out = Vec::new();
845    for entry in svc_entries {
846        let svc_dir = entry.path();
847        let service_toml = svc_dir.join("service.toml");
848        if !service_toml.exists() {
849            continue;
850        }
851        let service = match ServiceConfig::load(&service_toml) {
852            Ok(s) => s,
853            Err(e) => {
854                tracing::warn!(
855                    path = %service_toml.display(),
856                    error = %e,
857                    "skipping service with unparseable service.toml"
858                );
859                continue;
860            }
861        };
862
863        let mirrors_dir = svc_dir.join("mirrors");
864        if !mirrors_dir.exists() {
865            continue;
866        }
867        let mut mirror_entries: Vec<_> = std::fs::read_dir(&mirrors_dir)
868            .with_context(|| format!("reading {}", mirrors_dir.display()))?
869            .filter_map(|e| e.ok())
870            .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
871            .collect();
872        mirror_entries.sort_by_key(|e| e.file_name());
873
874        for m in mirror_entries {
875            let path = m.path();
876            let mirror = match MirrorConfig::load(&path) {
877                Ok(mc) => mc,
878                Err(e) => {
879                    tracing::warn!(
880                        path = %path.display(),
881                        error = %e,
882                        "skipping mirror with parse error"
883                    );
884                    continue;
885                }
886            };
887            out.push(LoadedMirror {
888                service: service.name.clone(),
889                env: path
890                    .file_stem()
891                    .and_then(|s| s.to_str())
892                    .unwrap_or_default()
893                    .to_string(),
894                path,
895                mirror,
896            });
897        }
898    }
899    Ok(out)
900}
901
902/// An ingress declaration that cannot become a front door.
903#[derive(Debug, Clone, PartialEq, Eq)]
904pub enum IngressProblem {
905    /// One mirror's own edges do not plan — a hole in its partition, a slot
906    /// claimed twice, a selector matching nothing.
907    Declaration {
908        service: String,
909        env: String,
910        mirror_toml: PathBuf,
911        detail: String,
912    },
913    /// Two services' edges cannot share the node they both front through.
914    /// Nothing but a cross-service pass can see this.
915    Collation { detail: String },
916    /// A declared edge that collates onto no node at all. Not fatal — the
917    /// passway arm deliberately renders a `<machine>` placeholder for an
918    /// operator to fill in — but it publishes nothing until it is placed.
919    Unplaced { label: String },
920}
921
922impl IngressProblem {
923    /// `true` for the problems that make the declaration tree incoherent, as
924    /// opposed to merely incomplete.
925    pub fn is_fatal(&self) -> bool {
926        !matches!(self, Self::Unplaced { .. })
927    }
928
929    /// Human-readable finding naming the file to open.
930    pub fn message(&self) -> String {
931        match self {
932            Self::Declaration {
933                service,
934                env,
935                mirror_toml,
936                detail,
937            } => format!(
938                "{service}/{env}: ingress declaration does not plan — {detail}\n\u{2192} {}",
939                mirror_toml.display()
940            ),
941            Self::Collation { detail } => format!(
942                "ingress edges from two services collide on a shared node — {detail}\n\
943                 \u{2192} the node's front door is COLLATED from every service that fronts \
944                 through it (W305 F2), so this is invisible from either mirror alone."
945            ),
946            Self::Unplaced { label } => format!(
947                "{label}: declares a front door with no machine to run it on — neither the \
948                 edge's `machines` nor the fronted slot's placement names a node, so it \
949                 publishes nothing.\n\u{2192} add `machines = [...]` to the edge."
950            ),
951        }
952    }
953}
954
955/// What every node in the camp must front, derived from every service's edges.
956#[derive(Debug, Clone, Default)]
957pub struct IngressReport {
958    /// The per-node front doors, empty when nothing collated.
959    pub collation: crate::reconciler::Collation,
960    /// Findings, fatal ones first-class via [`IngressProblem::is_fatal`].
961    pub problems: Vec<IngressProblem>,
962}
963
964/// Collate every service's declared ingress edges into the per-node front doors
965/// they imply (W305 F2 — the node-controller half).
966///
967/// **This is the direction that makes the node's front door derived rather than
968/// declared.** A service says which edges front it; this walks every service and
969/// answers the node's question — *what am I running, and for whom* — without the
970/// node declaring anything. The old shape had the node carry its own
971/// `MachineConfig.cloudflared` cohort statement (W267 Gap 3), which nothing
972/// checked against the services that actually fronted through it.
973///
974/// Upstreams are resolved **from configuration, never from the network**
975/// (R844-F12). A rule that pins `upstream_host` keeps it; every other rule takes
976/// the declared `[registration].mesh_ipv4` of each machine in its placement set,
977/// via [`machine_mesh_addrs`](crate::reconciler::machine_mesh_addrs). That is a
978/// second lookup over the machine slice this function already loaded — no
979/// network, no credentials, so this still answers the same question in CI as on
980/// the operator's laptop.
981///
982/// It is also what lets a mirror drop `upstream_host` at all: before this, the
983/// pin was the only thing standing between the apex and a collation that
984/// rendered `<unresolved>`. What it is *not* is a source of truth — see
985/// [`IngressPlan::resolve_upstreams_from_config`](crate::reconciler::IngressPlan::resolve_upstreams_from_config).
986/// A rule placed on a machine that declares no mesh address still collates fine
987/// and simply has no address yet.
988pub fn collate_workspace_ingress(workspace_root: &Path) -> anyhow::Result<IngressReport> {
989    // Needed only to resolve a slot's `required = { … }` into a machine name
990    // (R772) — `plan_ingress` itself stays pure. Reads the fleet inventory
991    // rather than going through the full `CloudConfig::load`: this walk
992    // collates every mirror in the workspace, and has no business hard-failing
993    // over an unrelated mirror's `providers.X.use = "<id>"` typo, which
994    // cross-ref validation would do.
995    //
996    // R870-B13: the *inventory*, not the camp-local machine files. A borrowing
997    // camp declares no machines of its own, so the camp-local loader resolved
998    // `required = { regions, mesh_tags }` against an empty candidate set there
999    // — which is exactly why such a camp has to pin `machines = [...]` by name
1000    // instead of declaring constraints. Reading the inventory is what makes
1001    // constraint-based placement expressible in a borrowing camp at all.
1002    let machines: Vec<MachineConfig> =
1003        crate::config::resolve_fleet_inventory(workspace_root)?.machines;
1004
1005    // R844-F12: name -> declared mesh address, built once for the whole walk.
1006    // Same slice, second lookup — the offline stand-in for the discovery read
1007    // this pass deliberately cannot make.
1008    let mesh_addrs = crate::reconciler::machine_mesh_addrs(&machines);
1009
1010    let mut planned = Vec::new();
1011    let mut problems = Vec::new();
1012
1013    for m in load_service_mirrors(workspace_root)? {
1014        let placements = match crate::reconciler::resolve_ingress_placements(&machines, &m.mirror) {
1015            Ok(p) => p,
1016            Err(e) => {
1017                problems.push(IngressProblem::Declaration {
1018                    service: m.service.clone(),
1019                    env: m.env.clone(),
1020                    mirror_toml: m.path.clone(),
1021                    detail: format!("{e:#}"),
1022                });
1023                continue;
1024            }
1025        };
1026        match crate::reconciler::plan_ingress(&m.mirror, &placements) {
1027            Ok(plans) => planned.extend(plans.into_iter().map(|mut plan| {
1028                plan.resolve_upstreams_from_config(&mesh_addrs);
1029                crate::reconciler::PlannedEdge {
1030                    service: m.service.clone(),
1031                    env: m.env.clone(),
1032                    plan,
1033                }
1034            })),
1035            Err(e) => problems.push(IngressProblem::Declaration {
1036                service: m.service.clone(),
1037                env: m.env.clone(),
1038                mirror_toml: m.path.clone(),
1039                detail: format!("{e:#}"),
1040            }),
1041        }
1042    }
1043
1044    let collation = match crate::reconciler::collate_front_doors(&planned) {
1045        Ok(c) => c,
1046        Err(e) => {
1047            problems.push(IngressProblem::Collation {
1048                detail: format!("{e:#}"),
1049            });
1050            Default::default()
1051        }
1052    };
1053    for label in &collation.unplaced {
1054        problems.push(IngressProblem::Unplaced {
1055            label: label.clone(),
1056        });
1057    }
1058
1059    Ok(IngressReport {
1060        collation,
1061        problems,
1062    })
1063}
1064
1065// ── Tests ──────────────────────────────────────────────────────────────────
1066
1067#[cfg(test)]
1068mod tests {
1069    use super::*;
1070    use tempfile::tempdir;
1071
1072    fn write_service(workspace: &Path, svc_name: &str, component_path: &str) {
1073        let svc_dir = workspace.join(".yah/services").join(svc_name);
1074        std::fs::create_dir_all(&svc_dir).unwrap();
1075        let toml = format!(
1076            "schema_version = 1\nname = \"{svc_name}\"\ndomain = \"{svc_name}.example.com\"\n\
1077             [[components]]\nid = \"models\"\nkind = \"static-asset\"\n\
1078             path = \"{component_path}\"\nrole = \"static\"\n"
1079        );
1080        std::fs::write(svc_dir.join("service.toml"), toml).unwrap();
1081    }
1082
1083    fn write_workload_with_aliases(dir: &Path, aliases: &[(&str, &str)]) {
1084        std::fs::create_dir_all(dir).unwrap();
1085        let alias_lines: String = aliases
1086            .iter()
1087            .map(|(k, v)| format!("\"{k}\" = \"{v}\"\n"))
1088            .collect();
1089        let content = format!(
1090            "kind = \"static-asset\"\nschema_version = \"V1\"\n\
1091             [aliases]\n{alias_lines}"
1092        );
1093        std::fs::write(dir.join("workload.toml"), content).unwrap();
1094    }
1095
1096    #[test]
1097    fn cloud_validate_clean_workspace_returns_empty() {
1098        let dir = tempdir().unwrap();
1099        let root = dir.path();
1100
1101        write_service(root, "svc-a", "svc-a/models");
1102        write_workload_with_aliases(
1103            &root.join("svc-a/models"),
1104            &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1105        );
1106
1107        let collisions = check_alias_collisions(root).unwrap();
1108        assert!(
1109            collisions.is_empty(),
1110            "expected no collisions: {collisions:?}"
1111        );
1112    }
1113
1114    #[test]
1115    fn cloud_validate_rejects_alias_collision() {
1116        let dir = tempdir().unwrap();
1117        let root = dir.path();
1118
1119        write_service(root, "svc-a", "svc-a/models");
1120        write_workload_with_aliases(
1121            &root.join("svc-a/models"),
1122            &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1123        );
1124
1125        write_service(root, "svc-b", "svc-b/models");
1126        write_workload_with_aliases(
1127            &root.join("svc-b/models"),
1128            &[("whisper-default-ggml", "svc-b/whisper/model.bin")],
1129        );
1130
1131        let collisions = check_alias_collisions(root).unwrap();
1132        assert_eq!(
1133            collisions.len(),
1134            1,
1135            "expected one collision: {collisions:?}"
1136        );
1137        let c = &collisions[0];
1138        assert_eq!(c.alias, "whisper-default-ggml");
1139        assert_eq!(c.first.service, "svc-a");
1140        assert_eq!(c.second.service, "svc-b");
1141
1142        let msg = c.message();
1143        assert!(msg.contains("whisper-default-ggml"), "message: {msg}");
1144        assert!(msg.contains("svc-a"), "message: {msg}");
1145        assert!(msg.contains("svc-b"), "message: {msg}");
1146    }
1147
1148    #[test]
1149    fn cloud_validate_distinct_aliases_no_collision() {
1150        let dir = tempdir().unwrap();
1151        let root = dir.path();
1152
1153        write_service(root, "svc-a", "svc-a/models");
1154        write_workload_with_aliases(
1155            &root.join("svc-a/models"),
1156            &[
1157                ("whisper-default-ggml", "svc-a/model.bin"),
1158                ("whisper-default", "svc-a/model.bin"),
1159            ],
1160        );
1161
1162        write_service(root, "svc-b", "svc-b/models");
1163        write_workload_with_aliases(
1164            &root.join("svc-b/models"),
1165            &[("whisper-default-coreml", "svc-b/model.tar.gz")],
1166        );
1167
1168        let collisions = check_alias_collisions(root).unwrap();
1169        assert!(collisions.is_empty());
1170    }
1171
1172    #[test]
1173    fn cloud_validate_multiple_collisions_all_reported() {
1174        let dir = tempdir().unwrap();
1175        let root = dir.path();
1176
1177        write_service(root, "svc-a", "svc-a/models");
1178        write_workload_with_aliases(
1179            &root.join("svc-a/models"),
1180            &[
1181                ("alias-one", "svc-a/one.bin"),
1182                ("alias-two", "svc-a/two.bin"),
1183            ],
1184        );
1185
1186        write_service(root, "svc-b", "svc-b/models");
1187        write_workload_with_aliases(
1188            &root.join("svc-b/models"),
1189            &[
1190                ("alias-one", "svc-b/one.bin"),
1191                ("alias-two", "svc-b/two.bin"),
1192            ],
1193        );
1194
1195        let collisions = check_alias_collisions(root).unwrap();
1196        assert_eq!(collisions.len(), 2);
1197        let names: Vec<_> = collisions.iter().map(|c| c.alias.as_str()).collect();
1198        assert!(names.contains(&"alias-one"));
1199        assert!(names.contains(&"alias-two"));
1200    }
1201
1202    #[test]
1203    fn cloud_validate_missing_services_dir_is_not_error() {
1204        let dir = tempdir().unwrap();
1205        let collisions = check_alias_collisions(dir.path()).unwrap();
1206        assert!(collisions.is_empty());
1207    }
1208
1209    #[test]
1210    fn cloud_validate_non_static_asset_workloads_ignored() {
1211        let dir = tempdir().unwrap();
1212        let root = dir.path();
1213
1214        write_service(root, "svc-a", "svc-a/api");
1215        // Write a container workload — no [aliases] block, should be silently skipped.
1216        let workload_dir = root.join("svc-a/api");
1217        std::fs::create_dir_all(&workload_dir).unwrap();
1218        // Just make the kind non-static-asset to ensure we skip it.
1219        // (Writes a valid mesofact-static workload which has no aliases)
1220        std::fs::write(
1221            workload_dir.join("workload.toml"),
1222            "schema_version = \"V1\"\nname = \"api\"\nkind = \"mesofact-static\"\n\
1223             bundle_dir = \"dist\"\n",
1224        )
1225        .unwrap();
1226
1227        let collisions = check_alias_collisions(root).unwrap();
1228        assert!(collisions.is_empty());
1229    }
1230
1231    // ── Port collisions (R602-B4) ────────────────────────────────────────────
1232
1233    fn write_mirror(workspace: &Path, svc: &str, env: &str, body: &str) {
1234        let dir = workspace.join(".yah/services").join(svc).join("mirrors");
1235        std::fs::create_dir_all(&dir).unwrap();
1236        std::fs::write(dir.join(format!("{env}.toml")), body).unwrap();
1237    }
1238
1239    fn local_static_mirror(port: u16) -> String {
1240        format!(
1241            "schema_version = 1\nshape = \"local\"\n\
1242             [providers.static]\nkind = \"local-static\"\nport = {port}\n"
1243        )
1244    }
1245
1246    #[test]
1247    fn port_collision_across_services_and_envs_is_flagged() {
1248        let dir = tempdir().unwrap();
1249        let root = dir.path();
1250        write_service(root, "scrabcake", "scrabcake/site");
1251        write_mirror(root, "scrabcake", "dev", &local_static_mirror(4322));
1252        write_service(root, "yah-marketing", "yah-marketing/site");
1253        write_mirror(
1254            root,
1255            "yah-marketing",
1256            "pond",
1257            "schema_version = 1\nshape = \"local\"\n\
1258             [providers.static]\nkind = \"miniflare-container\"\nport = 4322\n",
1259        );
1260
1261        let cols = check_port_collisions(root).unwrap();
1262        assert_eq!(cols.len(), 1, "{cols:?}");
1263        assert_eq!(cols[0].port, 4322);
1264        // Deterministic: "scrabcake" sorts before "yah-marketing".
1265        assert_eq!(cols[0].first.service, "scrabcake");
1266        assert_eq!(cols[0].second.service, "yah-marketing");
1267        assert!(cols[0].is_cross_service(), "different services collide");
1268        let msg = cols[0].message();
1269        assert!(msg.contains("4322"), "{msg}");
1270        assert!(msg.contains("scrabcake"), "{msg}");
1271        assert!(msg.contains("yah-marketing"), "{msg}");
1272    }
1273
1274    #[test]
1275    fn distinct_ports_no_collision() {
1276        let dir = tempdir().unwrap();
1277        let root = dir.path();
1278        write_service(root, "a", "a/site");
1279        write_mirror(root, "a", "dev", &local_static_mirror(4322));
1280        write_service(root, "b", "b/site");
1281        write_mirror(root, "b", "dev", &local_static_mirror(4323));
1282        assert!(check_port_collisions(root).unwrap().is_empty());
1283    }
1284
1285    #[test]
1286    fn same_service_two_envs_reusing_a_port_is_flagged() {
1287        // The ticket's "scrabcake cloud+dev reuse <port> twice more" shape.
1288        let dir = tempdir().unwrap();
1289        let root = dir.path();
1290        write_service(root, "scrabcake", "scrabcake/site");
1291        write_mirror(root, "scrabcake", "dev", &local_static_mirror(4352));
1292        write_mirror(root, "scrabcake", "cloud", &local_static_mirror(4352));
1293        let cols = check_port_collisions(root).unwrap();
1294        assert_eq!(cols.len(), 1, "{cols:?}");
1295        assert_eq!(cols[0].port, 4352);
1296        // "cloud" sorts before "dev".
1297        assert_eq!(cols[0].first.env, "cloud");
1298        assert_eq!(cols[0].second.env, "dev");
1299        assert!(
1300            !cols[0].is_cross_service(),
1301            "same service across envs is NOT cross-service"
1302        );
1303    }
1304
1305    #[test]
1306    fn reference_slots_do_not_bind_localhost_and_are_ignored() {
1307        // A `use = "..."` reference slot points at a cloud provider — reusing a
1308        // `port` field there is not a localhost collision.
1309        let dir = tempdir().unwrap();
1310        let root = dir.path();
1311        let ref_slot = "schema_version = 1\nshape = \"local\"\n\
1312             [providers.static]\nuse = \"cloudflare\"\nport = 8080\n";
1313        write_service(root, "a", "a/site");
1314        write_mirror(root, "a", "cloud", ref_slot);
1315        write_service(root, "b", "b/site");
1316        write_mirror(root, "b", "cloud", ref_slot);
1317        assert!(check_port_collisions(root).unwrap().is_empty());
1318    }
1319
1320    #[test]
1321    fn minio_api_and_console_ports_collide_across_ponds() {
1322        // Two pond MinIO slots on the same api_port bind the same host port.
1323        let dir = tempdir().unwrap();
1324        let root = dir.path();
1325        let minio = "schema_version = 1\nshape = \"local\"\n\
1326             [providers.object_store]\nkind = \"minio-container\"\napi_port = 9000\nconsole_port = 9001\n";
1327        write_service(root, "a", "a/site");
1328        write_mirror(root, "a", "pond", minio);
1329        write_service(root, "b", "b/site");
1330        write_mirror(root, "b", "pond", minio);
1331        let cols = check_port_collisions(root).unwrap();
1332        // Both api_port (9000) and console_port (9001) collide.
1333        assert_eq!(cols.len(), 2, "{cols:?}");
1334        let ports: Vec<u16> = cols.iter().map(|c| c.port).collect();
1335        assert!(ports.contains(&9000));
1336        assert!(ports.contains(&9001));
1337    }
1338
1339    #[test]
1340    fn missing_services_dir_is_not_error_for_ports() {
1341        let dir = tempdir().unwrap();
1342        assert!(check_port_collisions(dir.path()).unwrap().is_empty());
1343    }
1344
1345    // ── R763: the retired `tier:` arch mesh tag ────────────────────────────
1346
1347    /// Writes and re-parses the TOML immediately:
1348    /// [`load_camp_local_machine_tomls`] *skips* a file that fails to
1349    /// deserialize, so a fixture
1350    /// missing a required `MachineConfig` field would otherwise make every
1351    /// assert-empty test using it pass vacuously instead of failing loud.
1352    fn assert_machine_toml_parses(path: &Path) {
1353        let src = std::fs::read_to_string(path).unwrap();
1354        toml::from_str::<MachineConfig>(&src)
1355            .unwrap_or_else(|e| panic!("fixture {} does not parse as MachineConfig: {e}\n{src}", path.display()));
1356    }
1357
1358    fn write_machine_tags(workspace: &Path, name: &str, mesh_tags: &[&str]) {
1359        let dir = workspace.join(".yah/infra/machines");
1360        std::fs::create_dir_all(&dir).unwrap();
1361        let list: Vec<String> = mesh_tags.iter().map(|t| format!("\"{t}\"")).collect();
1362        let path = dir.join(format!("{name}.toml"));
1363        std::fs::write(
1364            &path,
1365            format!(
1366                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = [{}]\n",
1367                list.join(", ")
1368            ),
1369        )
1370        .unwrap();
1371        assert_machine_toml_parses(&path);
1372    }
1373
1374    #[test]
1375    fn the_current_arch_tag_is_clean() {
1376        let dir = tempdir().unwrap();
1377        write_machine_tags(dir.path(), "n1", &["tag:build-worker", "arch:x86", "os:linux"]);
1378        assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1379    }
1380
1381    #[test]
1382    fn a_stale_tier_arch_tag_is_flagged_with_its_replacement() {
1383        let dir = tempdir().unwrap();
1384        write_machine_tags(dir.path(), "n1", &["tag:build-worker", "tier:arm", "os:linux"]);
1385        let found = check_retired_arch_tags(dir.path()).unwrap();
1386        assert_eq!(found.len(), 1, "{found:?}");
1387        assert_eq!(found[0].tag, "tier:arm");
1388        assert_eq!(found[0].replacement(), "arch:arm");
1389        let msg = found[0].message();
1390        // The message has to carry the fix, not just the complaint — this lint
1391        // exists precisely because the symptom ("no node") names nothing.
1392        assert!(msg.contains("arch:arm"), "{msg}");
1393        assert!(msg.contains("n1"), "{msg}");
1394    }
1395
1396    /// The whole point: a node with the old tag is not *rejected* by placement,
1397    /// it silently stops being a candidate. Superset matching has no way to say
1398    /// "you asked for arch:x86 and I have tier:x86".
1399    #[test]
1400    fn a_stale_tag_is_not_caught_by_the_inert_taint_lint() {
1401        let dir = tempdir().unwrap();
1402        write_machine_tags(dir.path(), "n1", &["tier:x86"]);
1403        assert!(
1404            check_inert_taints(dir.path()).unwrap().is_empty(),
1405            "mesh tags are not taints — this needs its own check"
1406        );
1407        assert_eq!(check_retired_arch_tags(dir.path()).unwrap().len(), 1);
1408    }
1409
1410    #[test]
1411    fn missing_machines_dir_is_not_error_for_arch_tags() {
1412        let dir = tempdir().unwrap();
1413        assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1414    }
1415
1416    // ── R605-F12: sovereign members with no stated role ────────────────────
1417
1418    /// `stamp` is the sovereign declaration under test; everything else is the
1419    /// minimum a `MachineConfig` deserializes from. `assert_machine_toml_parses`
1420    /// catches a future edit here that drops a required field before it can
1421    /// produce a vacuously-passing assert-empty test (see that fn's doc).
1422    fn write_sovereign_machine(workspace: &Path, name: &str, stamp: &str) {
1423        let dir = workspace.join(".yah/infra/machines");
1424        std::fs::create_dir_all(&dir).unwrap();
1425        let path = dir.join(format!("{name}.toml"));
1426        std::fs::write(
1427            &path,
1428            format!("name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n{stamp}\n"),
1429        )
1430        .unwrap();
1431        assert_machine_toml_parses(&path);
1432    }
1433
1434    // ── R859-F2: the ingress floating IP declaration ──────────────────────
1435
1436    /// `provider` and the sovereign/floating-IP stamp are what vary; everything
1437    /// else is the minimum a `MachineConfig` deserializes from.
1438    fn write_fip_machine(workspace: &Path, name: &str, provider: &str, stamp: &str) {
1439        let dir = workspace.join(".yah/infra/machines");
1440        std::fs::create_dir_all(&dir).unwrap();
1441        let path = dir.join(format!("{name}.toml"));
1442        std::fs::write(
1443            &path,
1444            format!("name = \"{name}\"\nprovider = \"{provider}\"\nmesh_tags = []\n{stamp}\n"),
1445        )
1446        .unwrap();
1447        assert_machine_toml_parses(&path);
1448    }
1449
1450    /// The normal case, and it must be silent: most machines have no floating
1451    /// IP, and a lint that fires on every mesh-only box is one an operator
1452    /// learns to ignore.
1453    #[test]
1454    fn a_machine_with_no_ingress_floating_ip_is_never_flagged() {
1455        let dir = tempdir().unwrap();
1456        write_fip_machine(dir.path(), "us-west-002", "static", "");
1457        write_fip_machine(dir.path(), "n1", "digitalocean", "");
1458        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1459    }
1460
1461    #[test]
1462    fn a_valid_declaration_on_an_adapter_backed_provider_is_clean() {
1463        let dir = tempdir().unwrap();
1464        write_fip_machine(
1465            dir.path(),
1466            "us-west-001",
1467            "hetzner",
1468            "ingress_floating_ip = \"42\"",
1469        );
1470        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1471    }
1472
1473    /// The declaration that reads as a working failover path and is not one.
1474    #[test]
1475    fn a_provider_with_no_floating_ip_adapter_is_flagged_with_what_is_supported() {
1476        let dir = tempdir().unwrap();
1477        write_fip_machine(
1478            dir.path(),
1479            "us-east-001",
1480            "static",
1481            "ingress_floating_ip = \"51.81.85.200\"",
1482        );
1483        let found = check_ingress_floating_ip(dir.path()).unwrap();
1484        assert_eq!(found.len(), 1, "{found:?}");
1485        assert_eq!(
1486            found[0].kind,
1487            IngressFloatingIpProblemKind::NoAdapter {
1488                provider: "static".into()
1489            }
1490        );
1491        let msg = found[0].message();
1492        assert!(msg.contains("us-east-001"), "{msg}");
1493        assert!(msg.contains("hetzner"), "the message must name what IS supported: {msg}");
1494        assert!(msg.ends_with("us-east-001.toml"), "{msg}");
1495    }
1496
1497    /// An empty string is a declaration that resolves to nothing — distinct
1498    /// from omitting the key, which is the supported "no floating-IP path".
1499    #[test]
1500    fn a_blank_declaration_is_flagged_rather_than_read_as_absent() {
1501        let dir = tempdir().unwrap();
1502        write_fip_machine(
1503            dir.path(),
1504            "us-west-001",
1505            "hetzner",
1506            "ingress_floating_ip = \"  \"",
1507        );
1508        let found = check_ingress_floating_ip(dir.path()).unwrap();
1509        assert_eq!(found.len(), 1, "{found:?}");
1510        assert_eq!(found[0].kind, IngressFloatingIpProblemKind::Blank);
1511    }
1512
1513    /// The failure this check exists for: one IP moves between the boxes of a
1514    /// cohort, so two ids means a failover reassigns an IP that is not the one
1515    /// serving traffic — a flip that reports success and serves nothing.
1516    #[test]
1517    fn two_ids_in_one_sovereign_group_are_refused_and_name_both_machines() {
1518        let dir = tempdir().unwrap();
1519        write_fip_machine(
1520            dir.path(),
1521            "us-east-001",
1522            "hetzner",
1523            "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1524        );
1525        write_fip_machine(
1526            dir.path(),
1527            "us-west-001",
1528            "hetzner",
1529            "sovereign_group = \"prod\"\ningress_floating_ip = \"77\"",
1530        );
1531        let found = check_ingress_floating_ip(dir.path()).unwrap();
1532        assert_eq!(found.len(), 1, "{found:?}");
1533        let msg = found[0].message();
1534        assert!(msg.contains("us-west-001") && msg.contains("us-east-001"), "{msg}");
1535        assert!(msg.contains("42") && msg.contains("77"), "{msg}");
1536    }
1537
1538    #[test]
1539    fn one_id_across_a_whole_cohort_is_clean() {
1540        let dir = tempdir().unwrap();
1541        for name in ["us-east-001", "us-west-001", "us-south-001"] {
1542            write_fip_machine(
1543                dir.path(),
1544                name,
1545                "hetzner",
1546                "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1547            );
1548        }
1549        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1550    }
1551
1552    /// Two standalone boxes holding different IPs are two independent facts,
1553    /// not one contradiction — there is no cohort for them to disagree within.
1554    #[test]
1555    fn machines_in_no_group_may_hold_different_ips() {
1556        let dir = tempdir().unwrap();
1557        write_fip_machine(
1558            dir.path(),
1559            "us-east-001",
1560            "hetzner",
1561            "ingress_floating_ip = \"42\"",
1562        );
1563        write_fip_machine(
1564            dir.path(),
1565            "us-west-001",
1566            "vultr",
1567            "ingress_floating_ip = \"a-uuid\"",
1568        );
1569        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1570    }
1571
1572    #[test]
1573    fn missing_machines_dir_is_not_error_for_ingress_floating_ip() {
1574        let dir = tempdir().unwrap();
1575        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1576    }
1577
1578    #[test]
1579    fn a_group_with_no_role_is_reported_with_the_declaring_file() {
1580        let dir = tempdir().unwrap();
1581        let root = dir.path();
1582        write_sovereign_machine(root, "us-west-001", "sovereign_group = \"prod\"");
1583        let found = check_unroled_sovereign_members(root).unwrap();
1584        assert_eq!(found.len(), 1, "{found:?}");
1585        assert_eq!(found[0].machine, "us-west-001");
1586        assert_eq!(found[0].group, "prod");
1587        assert!(found[0].machine_toml.ends_with("us-west-001.toml"));
1588        let msg = found[0].message();
1589        // The message has to say what the silence currently means, or the
1590        // operator reads it as pedantry and adds the line without deciding.
1591        assert!(msg.contains("voter") && msg.contains("non-voter"), "{msg}");
1592        assert!(msg.contains("TOP-LEVEL"), "{msg}");
1593    }
1594
1595    #[test]
1596    fn either_stated_role_is_clean() {
1597        let dir = tempdir().unwrap();
1598        let root = dir.path();
1599        write_sovereign_machine(
1600            root,
1601            "us-west-001",
1602            "sovereign_group = \"prod\"\nsovereign_role = \"voter\"",
1603        );
1604        write_sovereign_machine(
1605            root,
1606            "us-west-003",
1607            "sovereign_group = \"prod\"\nsovereign_role = \"non-voter\"",
1608        );
1609        assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1610    }
1611
1612    /// A standalone box has no quorum to be eligible for, so demanding a role
1613    /// of it would be noise — and noise is what trains an operator to stop
1614    /// reading the check that matters.
1615    #[test]
1616    fn a_machine_in_no_group_is_not_asked_for_a_role() {
1617        let dir = tempdir().unwrap();
1618        let root = dir.path();
1619        write_sovereign_machine(root, "us-west-002", "taints = [\"no-appliance\"]");
1620        assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1621    }
1622
1623    #[test]
1624    fn unroled_findings_are_ordered_by_file_so_output_is_stable() {
1625        let dir = tempdir().unwrap();
1626        let root = dir.path();
1627        write_sovereign_machine(root, "b-node", "sovereign_group = \"dev\"");
1628        write_sovereign_machine(root, "a-node", "sovereign_group = \"prod\"");
1629        let found = check_unroled_sovereign_members(root).unwrap();
1630        let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1631        assert_eq!(names, vec!["a-node", "b-node"]);
1632    }
1633
1634    #[test]
1635    fn missing_machines_dir_is_not_error_for_unroled_members() {
1636        let dir = tempdir().unwrap();
1637        assert!(check_unroled_sovereign_members(dir.path())
1638            .unwrap()
1639            .is_empty());
1640    }
1641
1642    // ── R605-T10: LAN dial targets ─────────────────────────────────────────
1643
1644    /// `connect` is the raw body of the `[connect]` table; `registration` the
1645    /// raw body of `[registration]` (empty string omits the table).
1646    fn write_reach_machine(workspace: &Path, name: &str, connect: &str, registration: &str) {
1647        let dir = workspace.join(".yah/infra/machines");
1648        std::fs::create_dir_all(&dir).unwrap();
1649        let path = dir.join(format!("{name}.toml"));
1650        let reg = if registration.is_empty() {
1651            String::new()
1652        } else {
1653            format!("\n[registration]\n{registration}\n")
1654        };
1655        std::fs::write(
1656            &path,
1657            format!(
1658                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n\n\
1659                 [connect]\n{connect}\n{reg}"
1660            ),
1661        )
1662        .unwrap();
1663        assert_machine_toml_parses(&path);
1664    }
1665
1666    #[test]
1667    fn a_lan_literal_in_the_dialed_field_is_reported_with_its_file() {
1668        let dir = tempdir().unwrap();
1669        let root = dir.path();
1670        // us-west-011's shape at filing time: LAN literal, no mesh address.
1671        write_reach_machine(
1672            root,
1673            "us-west-011",
1674            "address = \"192.168.10.11\"\nssh = \"yah@192.168.10.11\"\n\
1675             identity_file = \"~/.ssh/yah\"\n\
1676             yubaba = \"http://192.168.10.11:7443\"",
1677            "",
1678        );
1679        let found = check_lan_dial_targets(root).unwrap();
1680        assert_eq!(found.len(), 1);
1681        assert_eq!(found[0].machine, "us-west-011");
1682        assert_eq!(found[0].url, "http://192.168.10.11:7443");
1683        assert_eq!(found[0].mesh_ipv4, None);
1684        let msg = found[0].message();
1685        assert!(msg.contains("mesh-join it"), "{msg}");
1686        assert!(msg.contains("us-west-011.toml"), "{msg}");
1687    }
1688
1689    /// A mesh-joined box gets the cheaper instruction, because the fix really
1690    /// is one deleted line — us-west-013/014's shape.
1691    #[test]
1692    fn a_mesh_joined_lan_declarer_is_told_to_delete_the_line() {
1693        let dir = tempdir().unwrap();
1694        let root = dir.path();
1695        write_reach_machine(
1696            root,
1697            "us-west-014",
1698            "address = \"192.168.10.14\"\nssh = \"yah@192.168.10.14\"\n\
1699             identity_file = \"~/.ssh/yah\"\n\
1700             yubaba = \"http://192.168.10.14:7443\"",
1701            "mesh_ipv4 = \"100.64.0.6\"",
1702        );
1703        let found = check_lan_dial_targets(root).unwrap();
1704        assert_eq!(found.len(), 1);
1705        assert_eq!(found[0].mesh_ipv4.as_deref(), Some("100.64.0.6"));
1706        let msg = found[0].message();
1707        assert!(msg.contains("DELETE the `yubaba` line"), "{msg}");
1708        assert!(msg.contains("100.64.0.6"), "{msg}");
1709    }
1710
1711    /// The three shapes that must NOT be flagged: a mesh address, the pre-mesh
1712    /// loopback placeholder, and a LAN address confined to the metadata fields
1713    /// (which is the whole point — the operator keeps it, automation ignores it).
1714    #[test]
1715    fn mesh_loopback_and_metadata_only_lan_addresses_are_clean() {
1716        let dir = tempdir().unwrap();
1717        let root = dir.path();
1718        write_reach_machine(
1719            root,
1720            "meshed",
1721            "address = \"192.168.10.15\"\nssh = \"yah@192.168.10.15\"\n\
1722             identity_file = \"~/.ssh/yah\"",
1723            "mesh_ipv4 = \"100.64.0.7\"",
1724        );
1725        write_reach_machine(
1726            root,
1727            "tunnelled",
1728            "address = \"192.168.10.16\"\nssh = \"yah@192.168.10.16\"\n\
1729             identity_file = \"~/.ssh/yah\"\n\
1730             yubaba = \"http://127.0.0.1:7443\"",
1731            "",
1732        );
1733        write_reach_machine(
1734            root,
1735            "public",
1736            "address = \"45.32.194.254\"\nssh = \"debian@45.32.194.254\"\n\
1737             identity_file = \"~/.ssh/yah\"\n\
1738             yubaba = \"http://45.32.194.254:7443\"",
1739            "",
1740        );
1741        assert!(check_lan_dial_targets(root).unwrap().is_empty());
1742    }
1743
1744    #[test]
1745    fn missing_machines_dir_is_not_error_for_lan_dial_targets() {
1746        let dir = tempdir().unwrap();
1747        assert!(check_lan_dial_targets(dir.path()).unwrap().is_empty());
1748    }
1749
1750    // ── R742-T4: inert taints ──────────────────────────────────────────────
1751
1752    fn write_machine(workspace: &Path, name: &str, taints: &[&str]) {
1753        let dir = workspace.join(".yah/infra/machines");
1754        std::fs::create_dir_all(&dir).unwrap();
1755        let list: Vec<String> = taints.iter().map(|t| format!("\"{t}\"")).collect();
1756        let path = dir.join(format!("{name}.toml"));
1757        std::fs::write(
1758            &path,
1759            format!(
1760                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\ntaints = [{}]\n",
1761                list.join(", ")
1762            ),
1763        )
1764        .unwrap();
1765        assert_machine_toml_parses(&path);
1766    }
1767
1768    #[test]
1769    fn archetype_repel_keys_and_affinity_keys_are_clean() {
1770        let dir = tempdir().unwrap();
1771        let root = dir.path();
1772        write_machine(root, "worker", &["no-server", "no-appliance", "no-job"]);
1773        write_machine(root, "edge", &["public-ip"]);
1774        write_machine(root, "plain", &[]);
1775        assert!(check_inert_taints(root).unwrap().is_empty());
1776    }
1777
1778    #[test]
1779    fn a_free_form_taint_is_reported_with_the_declaring_file() {
1780        let dir = tempdir().unwrap();
1781        let root = dir.path();
1782        // W305's headline example. Environment is not a taint.
1783        write_machine(root, "us-west-011", &["qa"]);
1784        let found = check_inert_taints(root).unwrap();
1785        assert_eq!(found.len(), 1, "{found:?}");
1786        assert_eq!(found[0].machine, "us-west-011");
1787        assert_eq!(found[0].key, "qa");
1788        assert!(found[0].machine_toml.ends_with("us-west-011.toml"));
1789        let msg = found[0].message();
1790        // The message has to carry the legal vocabulary, otherwise the
1791        // operator's only recourse is reading the scheduler.
1792        assert!(msg.contains("no-appliance"), "{msg}");
1793        assert!(msg.contains("public-ip"), "{msg}");
1794        assert!(msg.contains("mesh_tags"), "{msg}");
1795    }
1796
1797    #[test]
1798    fn no_voter_is_inert_because_voter_is_not_an_archetype() {
1799        // The one that cost real fleet state: it reads as an exclusion and
1800        // excludes nothing, so it sat on three nodes asserting a falsehood.
1801        let dir = tempdir().unwrap();
1802        let root = dir.path();
1803        write_machine(root, "us-west-015", &["no-server", "no-appliance", "no-voter"]);
1804        let found = check_inert_taints(root).unwrap();
1805        assert_eq!(found.len(), 1, "{found:?}");
1806        assert_eq!(found[0].key, "no-voter");
1807    }
1808
1809    #[test]
1810    fn findings_are_ordered_by_file_so_output_is_stable() {
1811        let dir = tempdir().unwrap();
1812        let root = dir.path();
1813        write_machine(root, "b-node", &["qa"]);
1814        write_machine(root, "a-node", &["staging"]);
1815        let found = check_inert_taints(root).unwrap();
1816        let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1817        assert_eq!(names, vec!["a-node", "b-node"]);
1818    }
1819
1820    #[test]
1821    fn missing_machines_dir_is_not_error() {
1822        let dir = tempdir().unwrap();
1823        assert!(check_inert_taints(dir.path()).unwrap().is_empty());
1824    }
1825
1826    #[test]
1827    fn an_unparseable_machine_toml_is_skipped_not_fatal() {
1828        let dir = tempdir().unwrap();
1829        let root = dir.path();
1830        let mdir = root.join(".yah/infra/machines");
1831        std::fs::create_dir_all(&mdir).unwrap();
1832        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1833        write_machine(root, "good", &["qa"]);
1834        // The broken file must not sink the sweep — a peer's half-written
1835        // scaffold is a normal state on a shared tree.
1836        let found = check_inert_taints(root).unwrap();
1837        assert_eq!(found.len(), 1);
1838        assert_eq!(found[0].machine, "good");
1839    }
1840
1841    // ── R787 / R870-B13: the two loaders' split contract ────────────────────
1842
1843    #[test]
1844    fn the_lint_loader_skips_an_unparseable_toml_and_still_pairs_the_path() {
1845        let dir = tempdir().unwrap();
1846        let root = dir.path();
1847        let mdir = root.join(".yah/infra/machines");
1848        std::fs::create_dir_all(&mdir).unwrap();
1849        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1850        write_machine(root, "good", &["qa"]);
1851
1852        let loaded = load_camp_local_machine_tomls(root).unwrap();
1853        assert_eq!(loaded.len(), 1, "{loaded:?}");
1854        assert_eq!(loaded[0].1.name, "good");
1855        assert!(loaded[0].0.ends_with("good.toml"));
1856    }
1857
1858    #[test]
1859    fn the_fleet_inventory_fails_the_whole_load_on_one_unparseable_camp_local_toml() {
1860        // The behavior collate_workspace_ingress relies on, now carried by
1861        // resolve_fleet_inventory rather than a Strict mode on the lint loader:
1862        // a bad machine toml this camp OWNS must not silently resolve a
1863        // `required` placement onto the wrong node.
1864        let dir = tempdir().unwrap();
1865        let root = dir.path();
1866        let mdir = root.join(".yah/infra/machines");
1867        std::fs::create_dir_all(&mdir).unwrap();
1868        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1869        write_machine(root, "good", &["qa"]);
1870
1871        let err = crate::config::resolve_fleet_inventory(root).unwrap_err();
1872        assert!(format!("{err:#}").contains("broken.toml"), "{err:#}");
1873    }
1874
1875    /// R870-B13, the defect this ticket was filed for, at the collation layer.
1876    ///
1877    /// A borrowing camp — empty `.yah/infra/machines/`, one `[[source]]` link
1878    /// to the owner's tree — must collate a mirror that pins a machine by name
1879    /// into a front door whose placement resolves. Before the fix,
1880    /// `collate_workspace_ingress` read the camp-local loader, saw an empty
1881    /// fleet, and every later name lookup failed on a machine that was
1882    /// declared all along one directory over.
1883    #[test]
1884    fn a_borrowing_camp_collates_a_front_door_on_a_machine_it_declares_nowhere() {
1885        let dir = tempdir().unwrap();
1886        // The owner camp, whose tree holds the only copy of the inventory.
1887        let owner = dir.path().join("owner");
1888        write_machine(&owner, "us-east-001", &["public-ip"]);
1889
1890        // The borrowing camp: no machines of its own, one link.
1891        let borrower = dir.path().join("borrower");
1892        std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
1893        std::fs::write(
1894            borrower.join(".yah/infra/sources.toml"),
1895            "schema_version = 1\n\
1896             [[source]]\n\
1897             owner = \"owner\"\n\
1898             kind = \"path\"\n\
1899             path = \"../owner\"\n\
1900             mode = \"read-only\"\n",
1901        )
1902        .unwrap();
1903
1904        // Control: the camp-local loader still sees nothing, which is correct
1905        // — that is what makes this a *borrowing* camp and not a copy.
1906        assert!(load_camp_local_machine_tomls(&borrower).unwrap().is_empty());
1907
1908        let inventory = crate::config::resolve_fleet_inventory(&borrower).unwrap();
1909        assert_eq!(
1910            inventory.machines.iter().map(|m| m.name.as_str()).collect::<Vec<_>>(),
1911            vec!["us-east-001"],
1912            "the borrowed fleet must resolve through sources.toml"
1913        );
1914        assert_eq!(
1915            inventory.origins.get("us-east-001").map(|o| o.owner.as_str()),
1916            Some("owner"),
1917            "a borrowed machine keeps its provenance"
1918        );
1919        assert_eq!(inventory.contributions.len(), 1);
1920        assert!(inventory.contributions[0].root_exists);
1921        assert_eq!(inventory.contributions[0].machines, 1);
1922
1923        // And the collation walk itself — the caller that regressed — resolves
1924        // the pinned name onto a real machine and collates a front door.
1925        write_service(&borrower, "marketing", "marketing/site");
1926        write_mirror(
1927            &borrower,
1928            "marketing",
1929            "cloud",
1930            &fronted_mirror("passway", &["us-east-001"], "api.example.com", 8080),
1931        );
1932        let report = collate_workspace_ingress(&borrower).unwrap();
1933        assert!(report.problems.is_empty(), "{:?}", report.problems);
1934        assert_eq!(
1935            report
1936                .collation
1937                .front_doors
1938                .iter()
1939                .map(|fd| fd.machine.as_str())
1940                .collect::<Vec<_>>(),
1941            vec!["us-east-001"],
1942        );
1943    }
1944
1945    /// R870-B13, the second thing the fix buys: **constraint-based placement
1946    /// becomes expressible in a borrowing camp.**
1947    ///
1948    /// `resolve_ingress_placements` resolves a slot's `required = { regions,
1949    /// mesh_tags }` against the machine slice this walk loads. With the
1950    /// camp-local loader that slice was empty in a borrowing camp, so the
1951    /// constraint matched nothing and the mirror failed to plan — which is
1952    /// exactly why such a camp had to pin `machines = [...]` by name instead.
1953    /// Reading the inventory removes that constraint on the constraint.
1954    #[test]
1955    fn a_borrowing_camp_can_place_by_constraint_rather_than_by_pin() {
1956        let dir = tempdir().unwrap();
1957        let owner = dir.path().join("owner");
1958        std::fs::create_dir_all(owner.join(".yah/infra/machines")).unwrap();
1959        std::fs::write(
1960            owner.join(".yah/infra/machines/us-east-001.toml"),
1961            "name = \"us-east-001\"\nprovider = \"static\"\nregion = \"us-east\"\n\
1962             mesh_tags = [\"tag:cloud-runner\"]\ntaints = [\"public-ip\"]\n",
1963        )
1964        .unwrap();
1965
1966        let borrower = dir.path().join("borrower");
1967        std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
1968        std::fs::write(
1969            borrower.join(".yah/infra/sources.toml"),
1970            "schema_version = 1\n[[source]]\nowner = \"owner\"\nkind = \"path\"\n\
1971             path = \"../owner\"\nmode = \"read-only\"\n",
1972        )
1973        .unwrap();
1974        write_service(&borrower, "marketing", "marketing/site");
1975        // No `machines` pin anywhere — placement is stated as a constraint.
1976        // The inline `required = { … }` form, never the `[providers.X.required]`
1977        // header form, which silently reparents every key below it (R772).
1978        write_mirror(
1979            &borrower,
1980            "marketing",
1981            "cloud",
1982            "schema_version = 1\nshape = \"single-machine\"\n\
1983             ingress = \"passway\"\ningress_machines = [\"us-east-001\"]\n\
1984             [providers.compute]\nuse = \"hetzner\"\nzone = \"api.example.com\"\n\
1985             port = 8080\n\
1986             required = { regions = [\"us-east\"], mesh_tags = [\"tag:cloud-runner\"] }\n",
1987        );
1988
1989        let report = collate_workspace_ingress(&borrower).unwrap();
1990        assert!(
1991            report.problems.is_empty(),
1992            "a constraint must resolve against the borrowed fleet: {:?}",
1993            report.problems
1994        );
1995        assert_eq!(
1996            report
1997                .collation
1998                .front_doors
1999                .iter()
2000                .map(|fd| fd.machine.as_str())
2001                .collect::<Vec<_>>(),
2002            vec!["us-east-001"],
2003        );
2004    }
2005
2006    /// The diagnostic half: a link that resolves to nothing must SAY so, since
2007    /// "no such machine" reads identically whether the camp declared no link,
2008    /// aimed one at a directory that is not a camp, or filtered the machine
2009    /// out with `select`.
2010    #[test]
2011    fn a_broken_link_is_reported_as_absent_rather_than_as_an_empty_fleet() {
2012        let dir = tempdir().unwrap();
2013        let borrower = dir.path().join("borrower");
2014        std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
2015        std::fs::write(
2016            borrower.join(".yah/infra/sources.toml"),
2017            "schema_version = 1\n\
2018             [[source]]\n\
2019             owner = \"owner\"\n\
2020             kind = \"path\"\n\
2021             path = \"../not-a-camp\"\n",
2022        )
2023        .unwrap();
2024
2025        let inventory = crate::config::resolve_fleet_inventory(&borrower).unwrap();
2026        assert!(inventory.machines.is_empty());
2027        assert!(!inventory.contributions[0].root_exists);
2028
2029        let described = inventory.describe_sources();
2030        assert!(described.contains("owner"), "{described}");
2031        assert!(described.contains("ABSENT"), "{described}");
2032    }
2033
2034    // ── R742-F2 (W305): workspace ingress collation ──
2035
2036    /// A mirror fronting one hostname through one edge on `machines`.
2037    fn fronted_mirror(provider: &str, machines: &[&str], hostname: &str, port: u16) -> String {
2038        let list = machines
2039            .iter()
2040            .map(|m| format!("\"{m}\""))
2041            .collect::<Vec<_>>()
2042            .join(", ");
2043        format!(
2044            "schema_version = 1\nshape = \"single-machine\"\n\
2045             ingress = \"{provider}\"\ningress_machines = [{list}]\n\
2046             [providers.compute]\nuse = \"hetzner\"\nzone = \"{hostname}\"\n\
2047             port = {port}\nupstream_host = \"100.64.0.5\"\n"
2048        )
2049    }
2050
2051    #[test]
2052    fn two_services_fronting_one_node_collate_into_one_front_door() {
2053        let dir = tempdir().unwrap();
2054        let root = dir.path();
2055        write_service(root, "yah-marketing", "yah-marketing/site");
2056        write_mirror(
2057            root,
2058            "yah-marketing",
2059            "cloud",
2060            &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
2061        );
2062        write_service(root, "yah-issues", "yah-issues/site");
2063        write_mirror(
2064            root,
2065            "yah-issues",
2066            "cloud",
2067            &fronted_mirror("passway", &["us-east-001"], "issues.yah.dev", 8731),
2068        );
2069
2070        let report = collate_workspace_ingress(root).unwrap();
2071        assert!(report.problems.is_empty(), "{:?}", report.problems);
2072        assert_eq!(report.collation.front_doors.len(), 1);
2073        let door = &report.collation.front_doors[0];
2074        assert_eq!(door.machine, "us-east-001");
2075        assert_eq!(
2076            door.passway_upstreams().unwrap(),
2077            vec!["issues.yah.dev=100.64.0.5:8731", "yah.dev=100.64.0.5:8080"]
2078        );
2079    }
2080
2081    #[test]
2082    fn a_cross_service_hostname_clash_is_reported_with_both_declarations() {
2083        // Neither service's own `yah cloud apply` can see this: each plans its
2084        // own mirror, both look fine, and the box ends up with whichever
2085        // applied last.
2086        let dir = tempdir().unwrap();
2087        let root = dir.path();
2088        write_service(root, "svc-a", "svc-a/site");
2089        write_mirror(
2090            root,
2091            "svc-a",
2092            "cloud",
2093            &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
2094        );
2095        write_service(root, "svc-b", "svc-b/site");
2096        write_mirror(
2097            root,
2098            "svc-b",
2099            "cloud",
2100            &fronted_mirror("cloudflare-tunnel", &["us-west-001"], "yah.dev", 8080),
2101        );
2102
2103        let report = collate_workspace_ingress(root).unwrap();
2104        let fatal: Vec<String> = report
2105            .problems
2106            .iter()
2107            .filter(|p| p.is_fatal())
2108            .map(|p| p.message())
2109            .collect();
2110        assert_eq!(fatal.len(), 1, "{fatal:?}");
2111        assert!(fatal[0].contains("svc-a/cloud"), "{}", fatal[0]);
2112        assert!(fatal[0].contains("svc-b/cloud"), "{}", fatal[0]);
2113    }
2114
2115    #[test]
2116    fn one_mirrors_broken_declaration_does_not_hide_the_rest() {
2117        // A malformed edge is reported against the file that declared it, and
2118        // the sweep continues — one service's typo must not blind the operator
2119        // to every other service's front doors.
2120        let dir = tempdir().unwrap();
2121        let root = dir.path();
2122        write_service(root, "svc-broken", "svc-broken/site");
2123        write_mirror(
2124            root,
2125            "svc-broken",
2126            "cloud",
2127            "schema_version = 1\nshape = \"single-machine\"\n\
2128             ingress_machines = [\"us-east-001\"]\n\
2129             [providers.compute]\nuse = \"hetzner\"\nzone = \"a.yah.dev\"\nport = 8080\n",
2130        );
2131        write_service(root, "svc-ok", "svc-ok/site");
2132        write_mirror(
2133            root,
2134            "svc-ok",
2135            "cloud",
2136            &fronted_mirror("passway", &["us-east-001"], "b.yah.dev", 8080),
2137        );
2138
2139        let report = collate_workspace_ingress(root).unwrap();
2140        assert_eq!(report.problems.len(), 1);
2141        assert!(
2142            report.problems[0].message().contains("svc-broken/cloud"),
2143            "{}",
2144            report.problems[0].message()
2145        );
2146        assert_eq!(report.collation.front_doors.len(), 1, "svc-ok still collates");
2147    }
2148
2149    #[test]
2150    fn a_mirror_with_no_ingress_contributes_nothing_and_is_not_a_finding() {
2151        let dir = tempdir().unwrap();
2152        let root = dir.path();
2153        write_service(root, "svc", "svc/site");
2154        write_mirror(root, "svc", "dev", &local_static_mirror(4322));
2155        let report = collate_workspace_ingress(root).unwrap();
2156        assert!(report.problems.is_empty());
2157        assert!(report.collation.front_doors.is_empty());
2158    }
2159}