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/// How [`load_machine_tomls`] handles a `.yah/infra/machines/*.toml` that
321/// fails to read or parse.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum MachineLoadMode {
324    /// Skip the file with a `tracing::warn!` and continue — the lint sweeps'
325    /// existing behavior. A peer's half-written scaffold on a shared tree
326    /// must not blind the sweep to every other finding it would report.
327    Tolerant,
328    /// Fail the whole load on the first read/parse error — for callers where
329    /// silently dropping a machine could produce a wrong-but-successful
330    /// result, e.g. [`collate_workspace_ingress`] resolving a `required`
331    /// placement constraint onto a node that isn't actually a candidate.
332    Strict,
333}
334
335/// Every `.yah/infra/machines/*.toml`, paired with the path that declared it
336/// — every lint finding names the file the operator opens, and
337/// [`collate_workspace_ingress`]'s placement resolution has no use for the
338/// path but takes it anyway rather than forcing a second loader to exist.
339///
340/// Missing `.yah/infra/machines/` is not an error — a fresh camp declares no
341/// nodes. Files are walked in sorted-filename order so callers that report
342/// findings (or resolve a first match) are deterministic across runs.
343pub fn load_machine_tomls(
344    workspace_root: &Path,
345    mode: MachineLoadMode,
346) -> anyhow::Result<Vec<(PathBuf, MachineConfig)>> {
347    let dir = machines_dir(workspace_root);
348    if !dir.exists() {
349        return Ok(Vec::new());
350    }
351
352    let mut entries: Vec<_> = std::fs::read_dir(&dir)
353        .with_context(|| format!("reading {}", dir.display()))?
354        .filter_map(|e| e.ok())
355        .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
356        .collect();
357    entries.sort_by_key(|e| e.file_name());
358
359    let mut out = Vec::with_capacity(entries.len());
360    for entry in entries {
361        let path = entry.path();
362        let src = match std::fs::read_to_string(&path) {
363            Ok(s) => s,
364            Err(e) if mode == MachineLoadMode::Tolerant => {
365                tracing::warn!(path = %path.display(), error = %e, "skipping unreadable machine toml");
366                continue;
367            }
368            Err(e) => return Err(e).with_context(|| format!("reading {}", path.display())),
369        };
370        let machine: MachineConfig = match toml::from_str(&src) {
371            Ok(m) => m,
372            Err(e) if mode == MachineLoadMode::Tolerant => {
373                tracing::warn!(path = %path.display(), error = %e, "skipping unparseable machine toml");
374                continue;
375            }
376            Err(e) => return Err(e).with_context(|| format!("parsing {}", path.display())),
377        };
378        out.push((path, machine));
379    }
380    Ok(out)
381}
382
383// ── R742-T4 (W305): inert node taints ──────────────────────────────────────
384
385/// A declared node taint no placement decision can read.
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct InertTaint {
388    /// Machine name as declared in the TOML.
389    pub machine: String,
390    /// Path to the declaring `.yah/infra/machines/<name>.toml`.
391    pub machine_toml: PathBuf,
392    /// The offending key.
393    pub key: String,
394}
395
396impl InertTaint {
397    pub fn message(&self) -> String {
398        format!(
399            "machine {:?} declares the taint {:?}, which no placement decision can read\n\
400             \u{2192} a taint fires in exactly two ways: as repulsion \
401             (`no-server` / `no-appliance` / `no-job`, an absolute block on that archetype), \
402             or as affinity (a key a workload names in `yah.placement.requires-taint`).\n\
403             \u{2192} legal keys today: {}\n\
404             \u{2192} if this is a *fact* about the node rather than a placement input, \
405             move it to `mesh_tags` or a comment; if it should really constrain placement, \
406             add it to cloud::config::AFFINITY_TAINT_KEYS together with the workload that \
407             requires it.\n  {}",
408            self.machine,
409            self.key,
410            live_taint_keys().join(", "),
411            self.machine_toml.display(),
412        )
413    }
414}
415
416/// Flag every taint in `.yah/infra/machines/*.toml` that the scheduler cannot
417/// act on (W305 finding 1 / R742-T4).
418///
419/// Why this is a *lint* and not a parse error: an inert taint breaks nothing.
420/// It changes no placement decision — that is the entire complaint. Refusing
421/// to deserialize would make an unrelated `yah cloud machine status` fail on a
422/// cosmetic problem, so the judgement is made where the operator asks for it.
423///
424/// **Camp-local machines only.** [`machines_dir`] is deliberately not the
425/// merged view `CloudConfig::load` builds from `.yah/infra/sources.toml` — a
426/// borrowed machine is declared in someone else's tree, where this camp cannot
427/// fix it, and a lint that cannot be resolved is noise that trains the
428/// operator to ignore the whole check.
429///
430/// Missing `.yah/infra/machines/` is not an error — a fresh camp declares no
431/// nodes. Unparseable files are skipped with a warning rather than aborting
432/// the sweep, matching [`check_port_collisions`]; whatever is wrong with them
433/// is a bigger problem than a taint key and surfaces on the load path.
434pub fn check_inert_taints(workspace_root: &Path) -> anyhow::Result<Vec<InertTaint>> {
435    let mut found = Vec::new();
436    for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
437        for key in machine.inert_taints() {
438            found.push(InertTaint {
439                machine: machine.name.clone(),
440                machine_toml: path.clone(),
441                key: key.to_string(),
442            });
443        }
444    }
445    Ok(found)
446}
447
448// ── R763 (W314): the retired `tier:` arch mesh tag ─────────────────────────
449
450/// The mesh-tag prefix that used to carry a build-worker's CPU architecture.
451/// Retired 2026-08-14 — the value was an architecture, not a tier, and it was
452/// squatting a prefix the environment axis wants.
453const RETIRED_ARCH_TAG_PREFIX: &str = "tier:";
454/// What it became. [`qed::platform::build_worker_mesh_tags`] emits this.
455const ARCH_TAG_PREFIX: &str = "arch:";
456
457/// A machine still declaring the retired `tier:<arch>` build-worker mesh tag.
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct RetiredArchTag {
460    pub machine: String,
461    pub machine_toml: PathBuf,
462    /// The offending tag, verbatim (e.g. `tier:x86`).
463    pub tag: String,
464}
465
466impl RetiredArchTag {
467    /// The replacement tag — same value, current prefix.
468    pub fn replacement(&self) -> String {
469        format!(
470            "{ARCH_TAG_PREFIX}{}",
471            self.tag.trim_start_matches(RETIRED_ARCH_TAG_PREFIX)
472        )
473    }
474
475    pub fn message(&self) -> String {
476        format!(
477            "machine {:?} declares the retired mesh tag {:?} — rename it to {:?}\n\
478             \u{2192} `tier:x86` / `tier:arm` were renamed to `arch:x86` / `arch:arm` \
479             (R763): the value is a CPU architecture, not a tier, and `tier:` is \
480             reserved for the environment axis.\n\
481             \u{2192} this does not fail loudly on its own, which is why it is checked \
482             here: `qed::platform::build_worker_mesh_tags` now requests `arch:<arch>`, \
483             and placement is SUPERSET matching, so a node still carrying the old tag \
484             simply stops matching and the build reports 'no node' instead of \
485             'wrong tag'.\n  {}",
486            self.machine,
487            self.tag,
488            self.replacement(),
489            self.machine_toml.display(),
490        )
491    }
492}
493
494/// Flag every machine still carrying a `tier:<arch>` build-worker mesh tag.
495///
496/// Same lint family, and the same camp-local-only scoping, as
497/// [`check_inert_taints`] — but note the failure it catches is *quieter* than
498/// an inert taint. An inert taint changes no decision; a stale arch tag changes
499/// the decision to "no candidate node", and the operator sees a placement
500/// failure with no hint that a tag rename caused it.
501pub fn check_retired_arch_tags(workspace_root: &Path) -> anyhow::Result<Vec<RetiredArchTag>> {
502    let mut found = Vec::new();
503    for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
504        for tag in machine
505            .mesh_tags
506            .iter()
507            .filter(|t| t.starts_with(RETIRED_ARCH_TAG_PREFIX))
508        {
509            found.push(RetiredArchTag {
510                machine: machine.name.clone(),
511                machine_toml: path.clone(),
512                tag: tag.clone(),
513            });
514        }
515    }
516    Ok(found)
517}
518
519// ── R605-F12: a group stamp with no role beside it ─────────────────────────
520
521/// A machine declaring `sovereign_group` without an explicit `sovereign_role`.
522#[derive(Debug, Clone, PartialEq, Eq)]
523pub struct UnroledSovereignMember {
524    pub machine: String,
525    pub machine_toml: PathBuf,
526    /// The group it declares — named in the message because the answer to
527    /// "voter or not" depends on which blast radius is being joined.
528    pub group: String,
529}
530
531impl UnroledSovereignMember {
532    pub fn message(&self) -> String {
533        format!(
534            "machine {:?} declares `sovereign_group = {:?}` but no `sovereign_role`\n\
535             \u{2192} membership and quorum eligibility are separate axes (R605-F12): a node can \
536             be inside a group's blast radius — its secrets, its upgrade cadence, its \
537             destruction — and still never hold a seat in its quorum.\n\
538             \u{2192} an absent role reads as `\"voter\"`, so this box is quorum-eligible today. \
539             That is the pre-R605-F12 meaning and is often right; the complaint is that nothing \
540             records whether anyone decided it.\n\
541             \u{2192} add `sovereign_role = \"voter\"` or `sovereign_role = \"non-voter\"` as a \
542             TOP-LEVEL key (below a `[table]` header TOML makes it a field of that table, and \
543             every consumer reads it as absent).\n  {}",
544            self.machine,
545            self.group,
546            self.machine_toml.display(),
547        )
548    }
549}
550
551/// Flag every machine that names a sovereign group without saying whether it
552/// votes in it (R605-F12).
553///
554/// Same lint family and the same camp-local-only scoping as
555/// [`check_inert_taints`], for a failure one step quieter than either of the
556/// others: an inert taint changes no decision and a stale arch tag changes it
557/// to "no candidate node", but an unwritten role changes nothing *visible* and
558/// silently grants a quorum seat. That is exactly the shape this ticket was
559/// opened about — a guarantee resting on which fields happen to be absent — so
560/// closing it by making the *other* absence load-bearing would have been the
561/// same bug with the sign flipped.
562///
563/// Why a lint rather than a required field: [`MachineConfig`] is deserialized
564/// from foreign trees too (`.yah/infra/sources.toml` overlays another camp's
565/// machines, which may predate this field entirely), and a hard parse error
566/// there is unfixable from here. The judgement is made where the operator asks
567/// for it, against machines this camp can actually edit.
568pub fn check_unroled_sovereign_members(
569    workspace_root: &Path,
570) -> anyhow::Result<Vec<UnroledSovereignMember>> {
571    let mut found = Vec::new();
572    for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
573        if let (Some(group), None) = (&machine.sovereign_group, &machine.sovereign_role) {
574            found.push(UnroledSovereignMember {
575                machine: machine.name.clone(),
576                machine_toml: path.clone(),
577                group: group.clone(),
578            });
579        }
580    }
581    Ok(found)
582}
583
584// ── R605-T10: a LAN literal in the field automation dials ──────────────────
585
586/// A machine whose `[connect].yubaba` points at an RFC1918 private address.
587#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct LanDialTarget {
589    pub machine: String,
590    pub machine_toml: PathBuf,
591    /// The offending URL, verbatim (e.g. `http://192.168.10.11:7443`).
592    pub url: String,
593    /// The registered mesh address, when the box has one — the difference
594    /// between "delete a line" and "go join the mesh first".
595    pub mesh_ipv4: Option<String>,
596}
597
598impl LanDialTarget {
599    pub fn message(&self) -> String {
600        let fix = match &self.mesh_ipv4 {
601            Some(ip) => format!(
602                "\u{2192} this box IS mesh-joined at {ip}: DELETE the `yubaba` line. \
603                 `[registration].mesh_ipv4` composes with `[connect].yubaba_port` on its own, \
604                 and `[connect].address` already records the LAN address as metadata."
605            ),
606            None => "\u{2192} this box has no `[registration].mesh_ipv4`: mesh-join it and record \
607                     the tailnet address, or taint it out of placement. Until then it is \
608                     unresolvable to automation and `MachineConfig::reach` refuses it by name."
609                .to_string(),
610        };
611        format!(
612            "machine {:?} declares `[connect].yubaba = {:?}` — a LAN address in the field every \
613             automated path dials\n\
614             \u{2192} the LAN address is an emergency break-glass route, never an official one \
615             (R605-T10, operator 2026-08-19). Automation ALWAYS assumes the caller is not on that \
616             LAN, and this camp genuinely is not — it sits on 192.168.22.0/22 with no route to \
617             192.168.10.0/24.\n\
618             \u{2192} it does not sit beside the mesh route, it OVERRODE it: before this check, a \
619             declared literal beat `[registration].mesh_ipv4` outright (R707-T6), so a healthy \
620             mesh-joined build worker was elected and then dialed at an address only its own \
621             building can reach.\n\
622             {fix}\n\
623             \u{2192} keeping the LAN address is fine and wanted — in `[connect].address` and \
624             `[connect].ssh`, which no resolver dials. A manual SSH session may use it once a \
625             human confirms they are on that LAN.\n  {}",
626            self.machine,
627            self.url,
628            self.machine_toml.display(),
629        )
630    }
631}
632
633/// Flag every machine that has put a LAN literal in `[connect].yubaba`.
634///
635/// Same lint family and camp-local-only scoping as [`check_inert_taints`]. The
636/// failure this one catches is the loudest of the four and still went unnoticed
637/// for weeks, which is the argument for checking it: the declaration parses, the
638/// node is elected by placement, and the only symptom is a connect timeout at
639/// dial time attributed to the box rather than to its file.
640///
641/// Loopback is deliberately not flagged — `http://127.0.0.1:7443` is the
642/// pre-mesh "reach me through the SSH tunnel" declaration, a genuine statement
643/// that `hub::coordinator::is_loopback_url` already judges downstream.
644///
645/// A finding here is now belt-and-braces rather than the only guard:
646/// [`MachineConfig::reach`] refuses to dial a private literal regardless. The
647/// lint exists so the operator hears about it from the file rather than from a
648/// roll that silently picked a different address than the one written down.
649pub fn check_lan_dial_targets(workspace_root: &Path) -> anyhow::Result<Vec<LanDialTarget>> {
650    let mut found = Vec::new();
651    for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
652        let Some(url) = machine.connect.as_ref().and_then(|c| c.yubaba.as_deref()) else {
653            continue;
654        };
655        if private_ipv4_from_url(url).is_none() {
656            continue;
657        }
658        found.push(LanDialTarget {
659            machine: machine.name.clone(),
660            machine_toml: path.clone(),
661            url: url.to_string(),
662            mesh_ipv4: machine.mesh_ipv4().map(str::to_string),
663        });
664    }
665    Ok(found)
666}
667
668// ── R859-F2: the ingress floating IP declaration ───────────────────────────
669
670/// A machine whose [`MachineConfig::ingress_floating_ip`] cannot mean what it
671/// says.
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct IngressFloatingIpProblem {
674    pub machine: String,
675    pub machine_toml: PathBuf,
676    pub kind: IngressFloatingIpProblemKind,
677}
678
679/// The three ways an `ingress_floating_ip` declaration can be wrong.
680#[derive(Debug, Clone, PartialEq, Eq)]
681pub enum IngressFloatingIpProblemKind {
682    /// Present but blank — a key that reads as a declaration and is not one.
683    Blank,
684    /// The machine's `provider` has no floating-IP adapter, so nothing in this
685    /// codebase could ever move that IP.
686    NoAdapter { provider: String },
687    /// Another machine in the same `sovereign_group` names a *different*
688    /// ingress IP.
689    CohortDisagreement {
690        group: String,
691        this_ip: String,
692        other_machine: String,
693        other_ip: String,
694    },
695}
696
697impl IngressFloatingIpProblem {
698    pub fn message(&self) -> String {
699        let body = match &self.kind {
700            IngressFloatingIpProblemKind::Blank => {
701                "declares an empty `ingress_floating_ip`\n\
702                 \u{2192} an empty string is not \"no floating IP\" — omit the key entirely for \
703                 that, which is the normal case and a clean skip. A blank value reads as a \
704                 declaration and resolves to nothing."
705                    .to_string()
706            }
707            IngressFloatingIpProblemKind::NoAdapter { provider } => format!(
708                "declares an `ingress_floating_ip` but its provider is {provider:?}, which has \
709                 no floating-IP adapter\n\
710                 \u{2192} floating/reserved IPs are implemented for hetzner, ovh and vultr \
711                 (cloud::provider::floating_ip's registry). Nothing in this codebase can move \
712                 this IP, so the declaration is inert — and inert in the worst way, because it \
713                 reads as a working failover path.\n\
714                 \u{2192} if the box really does carry a public IP that never moves, that is \
715                 what the `public-ip` taint plus `[connect].address` already say."
716            ),
717            IngressFloatingIpProblemKind::CohortDisagreement {
718                group,
719                this_ip,
720                other_machine,
721                other_ip,
722            } => format!(
723                "declares `ingress_floating_ip = {this_ip:?}` but {other_machine} in the same \
724                 sovereign_group {group:?} declares {other_ip:?}\n\
725                 \u{2192} the ingress floating IP is ONE resource that moves between the boxes \
726                 of a cohort as ownership flips. Two ids means an ownership flip reassigns a \
727                 different IP than the one currently serving traffic — the old IP stays pointed \
728                 at the dead box and the new one was never in DNS.\n\
729                 \u{2192} the symptom is a failover that reports success and serves nothing, \
730                 which is why this is refused here rather than discovered during one."
731            ),
732        };
733        format!(
734            "machine {:?} {body}\n  {}",
735            self.machine,
736            self.machine_toml.display(),
737        )
738    }
739}
740
741/// Flag every `ingress_floating_ip` declaration that cannot do what it claims
742/// (R859-F2).
743///
744/// Same lint family and camp-local-only scoping as [`check_inert_taints`], for
745/// a failure that is quiet in the ordinary way and loud exactly once: nothing
746/// reads the field until public ingress moves, so a wrong declaration sits
747/// green for months and then fails during the one event it exists for.
748///
749/// **Absence is never a finding.** Most machines have no floating IP — mesh-only
750/// nodes, tunnel-fronted boxes, every provider without an adapter — and that is
751/// the normal shape, not an omission. Only a *present* declaration is judged.
752///
753/// Machines with no `sovereign_group` are exempt from the cohort check: with no
754/// group there is no cohort to disagree with, and two standalone boxes that
755/// happen to hold different IPs are two independent facts rather than one
756/// contradiction.
757pub fn check_ingress_floating_ip(
758    workspace_root: &Path,
759) -> anyhow::Result<Vec<IngressFloatingIpProblem>> {
760    let machines = load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)?;
761    let mut found = Vec::new();
762
763    // First declaration seen per group, in load order — the one every later
764    // member of that group is compared against, so a cohort of N disagreeing
765    // machines yields N-1 findings pointing at one anchor rather than N^2
766    // pairwise ones.
767    let mut anchor: BTreeMap<String, (String, String)> = BTreeMap::new();
768
769    for (path, machine) in &machines {
770        let Some(ip) = machine.ingress_floating_ip.as_deref() else {
771            continue;
772        };
773        let mut push = |kind| {
774            found.push(IngressFloatingIpProblem {
775                machine: machine.name.clone(),
776                machine_toml: path.clone(),
777                kind,
778            })
779        };
780        if ip.trim().is_empty() {
781            push(IngressFloatingIpProblemKind::Blank);
782            continue;
783        }
784        if !crate::provider::provider_has_floating_ip_adapter(&machine.provider) {
785            push(IngressFloatingIpProblemKind::NoAdapter {
786                provider: machine.provider.clone(),
787            });
788        }
789        let Some(group) = machine.sovereign_group.as_deref() else {
790            continue;
791        };
792        match anchor.get(group) {
793            None => {
794                anchor.insert(group.to_string(), (machine.name.clone(), ip.to_string()));
795            }
796            Some((other_machine, other_ip)) if other_ip != ip => {
797                push(IngressFloatingIpProblemKind::CohortDisagreement {
798                    group: group.to_string(),
799                    this_ip: ip.to_string(),
800                    other_machine: other_machine.clone(),
801                    other_ip: other_ip.clone(),
802                });
803            }
804            Some(_) => {}
805        }
806    }
807    Ok(found)
808}
809
810// ── R742-F2 (W305): ingress edge collation ─────────────────────────────────
811
812/// One mirror, loaded with the identity every finding has to be able to name.
813#[derive(Debug, Clone)]
814pub struct LoadedMirror {
815    /// Service name (matches `service.toml`'s `name` field).
816    pub service: String,
817    /// Environment (file stem of `mirrors/<env>.toml`).
818    pub env: String,
819    /// Path to the mirror TOML — what an operator opens to fix a finding.
820    pub path: PathBuf,
821    pub mirror: MirrorConfig,
822}
823
824/// Every `.yah/services/<svc>/mirrors/<env>.toml` in the workspace, in a
825/// deterministic order (services sorted, then envs).
826///
827/// Shared by every cross-mirror check, because "walk the mirrors" is the one
828/// part all of them agree on and three hand-rolled copies of it drift. A
829/// service with no `service.toml`, or a mirror that will not parse, is skipped
830/// with a warning rather than aborting the sweep — whatever is wrong with it is
831/// a bigger problem than the lint and surfaces on the load path.
832pub fn load_service_mirrors(workspace_root: &Path) -> anyhow::Result<Vec<LoadedMirror>> {
833    let dir = services_dir(workspace_root);
834    if !dir.exists() {
835        return Ok(vec![]);
836    }
837
838    let mut svc_entries: Vec<_> = std::fs::read_dir(&dir)
839        .with_context(|| format!("reading {}", dir.display()))?
840        .filter_map(|e| e.ok())
841        .filter(|e| e.path().is_dir())
842        .collect();
843    svc_entries.sort_by_key(|e| e.file_name());
844
845    let mut out = Vec::new();
846    for entry in svc_entries {
847        let svc_dir = entry.path();
848        let service_toml = svc_dir.join("service.toml");
849        if !service_toml.exists() {
850            continue;
851        }
852        let service = match ServiceConfig::load(&service_toml) {
853            Ok(s) => s,
854            Err(e) => {
855                tracing::warn!(
856                    path = %service_toml.display(),
857                    error = %e,
858                    "skipping service with unparseable service.toml"
859                );
860                continue;
861            }
862        };
863
864        let mirrors_dir = svc_dir.join("mirrors");
865        if !mirrors_dir.exists() {
866            continue;
867        }
868        let mut mirror_entries: Vec<_> = std::fs::read_dir(&mirrors_dir)
869            .with_context(|| format!("reading {}", mirrors_dir.display()))?
870            .filter_map(|e| e.ok())
871            .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
872            .collect();
873        mirror_entries.sort_by_key(|e| e.file_name());
874
875        for m in mirror_entries {
876            let path = m.path();
877            let mirror = match MirrorConfig::load(&path) {
878                Ok(mc) => mc,
879                Err(e) => {
880                    tracing::warn!(
881                        path = %path.display(),
882                        error = %e,
883                        "skipping mirror with parse error"
884                    );
885                    continue;
886                }
887            };
888            out.push(LoadedMirror {
889                service: service.name.clone(),
890                env: path
891                    .file_stem()
892                    .and_then(|s| s.to_str())
893                    .unwrap_or_default()
894                    .to_string(),
895                path,
896                mirror,
897            });
898        }
899    }
900    Ok(out)
901}
902
903/// An ingress declaration that cannot become a front door.
904#[derive(Debug, Clone, PartialEq, Eq)]
905pub enum IngressProblem {
906    /// One mirror's own edges do not plan — a hole in its partition, a slot
907    /// claimed twice, a selector matching nothing.
908    Declaration {
909        service: String,
910        env: String,
911        mirror_toml: PathBuf,
912        detail: String,
913    },
914    /// Two services' edges cannot share the node they both front through.
915    /// Nothing but a cross-service pass can see this.
916    Collation { detail: String },
917    /// A declared edge that collates onto no node at all. Not fatal — the
918    /// passway arm deliberately renders a `<machine>` placeholder for an
919    /// operator to fill in — but it publishes nothing until it is placed.
920    Unplaced { label: String },
921}
922
923impl IngressProblem {
924    /// `true` for the problems that make the declaration tree incoherent, as
925    /// opposed to merely incomplete.
926    pub fn is_fatal(&self) -> bool {
927        !matches!(self, Self::Unplaced { .. })
928    }
929
930    /// Human-readable finding naming the file to open.
931    pub fn message(&self) -> String {
932        match self {
933            Self::Declaration {
934                service,
935                env,
936                mirror_toml,
937                detail,
938            } => format!(
939                "{service}/{env}: ingress declaration does not plan — {detail}\n\u{2192} {}",
940                mirror_toml.display()
941            ),
942            Self::Collation { detail } => format!(
943                "ingress edges from two services collide on a shared node — {detail}\n\
944                 \u{2192} the node's front door is COLLATED from every service that fronts \
945                 through it (W305 F2), so this is invisible from either mirror alone."
946            ),
947            Self::Unplaced { label } => format!(
948                "{label}: declares a front door with no machine to run it on — neither the \
949                 edge's `machines` nor the fronted slot's placement names a node, so it \
950                 publishes nothing.\n\u{2192} add `machines = [...]` to the edge."
951            ),
952        }
953    }
954}
955
956/// What every node in the camp must front, derived from every service's edges.
957#[derive(Debug, Clone, Default)]
958pub struct IngressReport {
959    /// The per-node front doors, empty when nothing collated.
960    pub collation: crate::reconciler::Collation,
961    /// Findings, fatal ones first-class via [`IngressProblem::is_fatal`].
962    pub problems: Vec<IngressProblem>,
963}
964
965/// Collate every service's declared ingress edges into the per-node front doors
966/// they imply (W305 F2 — the node-controller half).
967///
968/// **This is the direction that makes the node's front door derived rather than
969/// declared.** A service says which edges front it; this walks every service and
970/// answers the node's question — *what am I running, and for whom* — without the
971/// node declaring anything. The old shape had the node carry its own
972/// `MachineConfig.cloudflared` cohort statement (W267 Gap 3), which nothing
973/// checked against the services that actually fronted through it.
974///
975/// Upstreams are resolved **from configuration, never from the network**
976/// (R844-F12). A rule that pins `upstream_host` keeps it; every other rule takes
977/// the declared `[registration].mesh_ipv4` of each machine in its placement set,
978/// via [`machine_mesh_addrs`](crate::reconciler::machine_mesh_addrs). That is a
979/// second lookup over the machine slice this function already loaded — no
980/// network, no credentials, so this still answers the same question in CI as on
981/// the operator's laptop.
982///
983/// It is also what lets a mirror drop `upstream_host` at all: before this, the
984/// pin was the only thing standing between the apex and a collation that
985/// rendered `<unresolved>`. What it is *not* is a source of truth — see
986/// [`IngressPlan::resolve_upstreams_from_config`](crate::reconciler::IngressPlan::resolve_upstreams_from_config).
987/// A rule placed on a machine that declares no mesh address still collates fine
988/// and simply has no address yet.
989pub fn collate_workspace_ingress(workspace_root: &Path) -> anyhow::Result<IngressReport> {
990    // Needed only to resolve a slot's `required = { … }` into a machine name
991    // (R772) — `plan_ingress` itself stays pure. Reads `.yah/infra/machines/`
992    // directly rather than going through the full `CloudConfig::load`: this
993    // walk collates every mirror in the workspace, and has no business
994    // hard-failing over an unrelated mirror's `providers.X.use = "<id>"` typo,
995    // which cross-ref validation would do. Strict mode: silently dropping a
996    // machine here could resolve a `required` constraint onto the wrong node
997    // with no error, unlike the tolerant lint sweeps below.
998    let machines: Vec<MachineConfig> =
999        load_machine_tomls(workspace_root, MachineLoadMode::Strict)?
1000            .into_iter()
1001            .map(|(_, m)| m)
1002            .collect();
1003
1004    // R844-F12: name -> declared mesh address, built once for the whole walk.
1005    // Same slice, second lookup — the offline stand-in for the discovery read
1006    // this pass deliberately cannot make.
1007    let mesh_addrs = crate::reconciler::machine_mesh_addrs(&machines);
1008
1009    let mut planned = Vec::new();
1010    let mut problems = Vec::new();
1011
1012    for m in load_service_mirrors(workspace_root)? {
1013        let placements = match crate::reconciler::resolve_ingress_placements(&machines, &m.mirror) {
1014            Ok(p) => p,
1015            Err(e) => {
1016                problems.push(IngressProblem::Declaration {
1017                    service: m.service.clone(),
1018                    env: m.env.clone(),
1019                    mirror_toml: m.path.clone(),
1020                    detail: format!("{e:#}"),
1021                });
1022                continue;
1023            }
1024        };
1025        match crate::reconciler::plan_ingress(&m.mirror, &placements) {
1026            Ok(plans) => planned.extend(plans.into_iter().map(|mut plan| {
1027                plan.resolve_upstreams_from_config(&mesh_addrs);
1028                crate::reconciler::PlannedEdge {
1029                    service: m.service.clone(),
1030                    env: m.env.clone(),
1031                    plan,
1032                }
1033            })),
1034            Err(e) => problems.push(IngressProblem::Declaration {
1035                service: m.service.clone(),
1036                env: m.env.clone(),
1037                mirror_toml: m.path.clone(),
1038                detail: format!("{e:#}"),
1039            }),
1040        }
1041    }
1042
1043    let collation = match crate::reconciler::collate_front_doors(&planned) {
1044        Ok(c) => c,
1045        Err(e) => {
1046            problems.push(IngressProblem::Collation {
1047                detail: format!("{e:#}"),
1048            });
1049            Default::default()
1050        }
1051    };
1052    for label in &collation.unplaced {
1053        problems.push(IngressProblem::Unplaced {
1054            label: label.clone(),
1055        });
1056    }
1057
1058    Ok(IngressReport {
1059        collation,
1060        problems,
1061    })
1062}
1063
1064// ── Tests ──────────────────────────────────────────────────────────────────
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069    use tempfile::tempdir;
1070
1071    fn write_service(workspace: &Path, svc_name: &str, component_path: &str) {
1072        let svc_dir = workspace.join(".yah/services").join(svc_name);
1073        std::fs::create_dir_all(&svc_dir).unwrap();
1074        let toml = format!(
1075            "schema_version = 1\nname = \"{svc_name}\"\ndomain = \"{svc_name}.example.com\"\n\
1076             [[components]]\nid = \"models\"\nkind = \"static-asset\"\n\
1077             path = \"{component_path}\"\nrole = \"static\"\n"
1078        );
1079        std::fs::write(svc_dir.join("service.toml"), toml).unwrap();
1080    }
1081
1082    fn write_workload_with_aliases(dir: &Path, aliases: &[(&str, &str)]) {
1083        std::fs::create_dir_all(dir).unwrap();
1084        let alias_lines: String = aliases
1085            .iter()
1086            .map(|(k, v)| format!("\"{k}\" = \"{v}\"\n"))
1087            .collect();
1088        let content = format!(
1089            "kind = \"static-asset\"\nschema_version = \"V1\"\n\
1090             [aliases]\n{alias_lines}"
1091        );
1092        std::fs::write(dir.join("workload.toml"), content).unwrap();
1093    }
1094
1095    #[test]
1096    fn cloud_validate_clean_workspace_returns_empty() {
1097        let dir = tempdir().unwrap();
1098        let root = dir.path();
1099
1100        write_service(root, "svc-a", "svc-a/models");
1101        write_workload_with_aliases(
1102            &root.join("svc-a/models"),
1103            &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1104        );
1105
1106        let collisions = check_alias_collisions(root).unwrap();
1107        assert!(
1108            collisions.is_empty(),
1109            "expected no collisions: {collisions:?}"
1110        );
1111    }
1112
1113    #[test]
1114    fn cloud_validate_rejects_alias_collision() {
1115        let dir = tempdir().unwrap();
1116        let root = dir.path();
1117
1118        write_service(root, "svc-a", "svc-a/models");
1119        write_workload_with_aliases(
1120            &root.join("svc-a/models"),
1121            &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1122        );
1123
1124        write_service(root, "svc-b", "svc-b/models");
1125        write_workload_with_aliases(
1126            &root.join("svc-b/models"),
1127            &[("whisper-default-ggml", "svc-b/whisper/model.bin")],
1128        );
1129
1130        let collisions = check_alias_collisions(root).unwrap();
1131        assert_eq!(
1132            collisions.len(),
1133            1,
1134            "expected one collision: {collisions:?}"
1135        );
1136        let c = &collisions[0];
1137        assert_eq!(c.alias, "whisper-default-ggml");
1138        assert_eq!(c.first.service, "svc-a");
1139        assert_eq!(c.second.service, "svc-b");
1140
1141        let msg = c.message();
1142        assert!(msg.contains("whisper-default-ggml"), "message: {msg}");
1143        assert!(msg.contains("svc-a"), "message: {msg}");
1144        assert!(msg.contains("svc-b"), "message: {msg}");
1145    }
1146
1147    #[test]
1148    fn cloud_validate_distinct_aliases_no_collision() {
1149        let dir = tempdir().unwrap();
1150        let root = dir.path();
1151
1152        write_service(root, "svc-a", "svc-a/models");
1153        write_workload_with_aliases(
1154            &root.join("svc-a/models"),
1155            &[
1156                ("whisper-default-ggml", "svc-a/model.bin"),
1157                ("whisper-default", "svc-a/model.bin"),
1158            ],
1159        );
1160
1161        write_service(root, "svc-b", "svc-b/models");
1162        write_workload_with_aliases(
1163            &root.join("svc-b/models"),
1164            &[("whisper-default-coreml", "svc-b/model.tar.gz")],
1165        );
1166
1167        let collisions = check_alias_collisions(root).unwrap();
1168        assert!(collisions.is_empty());
1169    }
1170
1171    #[test]
1172    fn cloud_validate_multiple_collisions_all_reported() {
1173        let dir = tempdir().unwrap();
1174        let root = dir.path();
1175
1176        write_service(root, "svc-a", "svc-a/models");
1177        write_workload_with_aliases(
1178            &root.join("svc-a/models"),
1179            &[
1180                ("alias-one", "svc-a/one.bin"),
1181                ("alias-two", "svc-a/two.bin"),
1182            ],
1183        );
1184
1185        write_service(root, "svc-b", "svc-b/models");
1186        write_workload_with_aliases(
1187            &root.join("svc-b/models"),
1188            &[
1189                ("alias-one", "svc-b/one.bin"),
1190                ("alias-two", "svc-b/two.bin"),
1191            ],
1192        );
1193
1194        let collisions = check_alias_collisions(root).unwrap();
1195        assert_eq!(collisions.len(), 2);
1196        let names: Vec<_> = collisions.iter().map(|c| c.alias.as_str()).collect();
1197        assert!(names.contains(&"alias-one"));
1198        assert!(names.contains(&"alias-two"));
1199    }
1200
1201    #[test]
1202    fn cloud_validate_missing_services_dir_is_not_error() {
1203        let dir = tempdir().unwrap();
1204        let collisions = check_alias_collisions(dir.path()).unwrap();
1205        assert!(collisions.is_empty());
1206    }
1207
1208    #[test]
1209    fn cloud_validate_non_static_asset_workloads_ignored() {
1210        let dir = tempdir().unwrap();
1211        let root = dir.path();
1212
1213        write_service(root, "svc-a", "svc-a/api");
1214        // Write a container workload — no [aliases] block, should be silently skipped.
1215        let workload_dir = root.join("svc-a/api");
1216        std::fs::create_dir_all(&workload_dir).unwrap();
1217        // Just make the kind non-static-asset to ensure we skip it.
1218        // (Writes a valid mesofact-static workload which has no aliases)
1219        std::fs::write(
1220            workload_dir.join("workload.toml"),
1221            "schema_version = \"V1\"\nname = \"api\"\nkind = \"mesofact-static\"\n\
1222             bundle_dir = \"dist\"\n",
1223        )
1224        .unwrap();
1225
1226        let collisions = check_alias_collisions(root).unwrap();
1227        assert!(collisions.is_empty());
1228    }
1229
1230    // ── Port collisions (R602-B4) ────────────────────────────────────────────
1231
1232    fn write_mirror(workspace: &Path, svc: &str, env: &str, body: &str) {
1233        let dir = workspace.join(".yah/services").join(svc).join("mirrors");
1234        std::fs::create_dir_all(&dir).unwrap();
1235        std::fs::write(dir.join(format!("{env}.toml")), body).unwrap();
1236    }
1237
1238    fn local_static_mirror(port: u16) -> String {
1239        format!(
1240            "schema_version = 1\nshape = \"local\"\n\
1241             [providers.static]\nkind = \"local-static\"\nport = {port}\n"
1242        )
1243    }
1244
1245    #[test]
1246    fn port_collision_across_services_and_envs_is_flagged() {
1247        let dir = tempdir().unwrap();
1248        let root = dir.path();
1249        write_service(root, "scrabcake", "scrabcake/site");
1250        write_mirror(root, "scrabcake", "dev", &local_static_mirror(4322));
1251        write_service(root, "yah-marketing", "yah-marketing/site");
1252        write_mirror(
1253            root,
1254            "yah-marketing",
1255            "pond",
1256            "schema_version = 1\nshape = \"local\"\n\
1257             [providers.static]\nkind = \"miniflare-container\"\nport = 4322\n",
1258        );
1259
1260        let cols = check_port_collisions(root).unwrap();
1261        assert_eq!(cols.len(), 1, "{cols:?}");
1262        assert_eq!(cols[0].port, 4322);
1263        // Deterministic: "scrabcake" sorts before "yah-marketing".
1264        assert_eq!(cols[0].first.service, "scrabcake");
1265        assert_eq!(cols[0].second.service, "yah-marketing");
1266        assert!(cols[0].is_cross_service(), "different services collide");
1267        let msg = cols[0].message();
1268        assert!(msg.contains("4322"), "{msg}");
1269        assert!(msg.contains("scrabcake"), "{msg}");
1270        assert!(msg.contains("yah-marketing"), "{msg}");
1271    }
1272
1273    #[test]
1274    fn distinct_ports_no_collision() {
1275        let dir = tempdir().unwrap();
1276        let root = dir.path();
1277        write_service(root, "a", "a/site");
1278        write_mirror(root, "a", "dev", &local_static_mirror(4322));
1279        write_service(root, "b", "b/site");
1280        write_mirror(root, "b", "dev", &local_static_mirror(4323));
1281        assert!(check_port_collisions(root).unwrap().is_empty());
1282    }
1283
1284    #[test]
1285    fn same_service_two_envs_reusing_a_port_is_flagged() {
1286        // The ticket's "scrabcake cloud+dev reuse <port> twice more" shape.
1287        let dir = tempdir().unwrap();
1288        let root = dir.path();
1289        write_service(root, "scrabcake", "scrabcake/site");
1290        write_mirror(root, "scrabcake", "dev", &local_static_mirror(4352));
1291        write_mirror(root, "scrabcake", "cloud", &local_static_mirror(4352));
1292        let cols = check_port_collisions(root).unwrap();
1293        assert_eq!(cols.len(), 1, "{cols:?}");
1294        assert_eq!(cols[0].port, 4352);
1295        // "cloud" sorts before "dev".
1296        assert_eq!(cols[0].first.env, "cloud");
1297        assert_eq!(cols[0].second.env, "dev");
1298        assert!(
1299            !cols[0].is_cross_service(),
1300            "same service across envs is NOT cross-service"
1301        );
1302    }
1303
1304    #[test]
1305    fn reference_slots_do_not_bind_localhost_and_are_ignored() {
1306        // A `use = "..."` reference slot points at a cloud provider — reusing a
1307        // `port` field there is not a localhost collision.
1308        let dir = tempdir().unwrap();
1309        let root = dir.path();
1310        let ref_slot = "schema_version = 1\nshape = \"local\"\n\
1311             [providers.static]\nuse = \"cloudflare\"\nport = 8080\n";
1312        write_service(root, "a", "a/site");
1313        write_mirror(root, "a", "cloud", ref_slot);
1314        write_service(root, "b", "b/site");
1315        write_mirror(root, "b", "cloud", ref_slot);
1316        assert!(check_port_collisions(root).unwrap().is_empty());
1317    }
1318
1319    #[test]
1320    fn minio_api_and_console_ports_collide_across_ponds() {
1321        // Two pond MinIO slots on the same api_port bind the same host port.
1322        let dir = tempdir().unwrap();
1323        let root = dir.path();
1324        let minio = "schema_version = 1\nshape = \"local\"\n\
1325             [providers.object_store]\nkind = \"minio-container\"\napi_port = 9000\nconsole_port = 9001\n";
1326        write_service(root, "a", "a/site");
1327        write_mirror(root, "a", "pond", minio);
1328        write_service(root, "b", "b/site");
1329        write_mirror(root, "b", "pond", minio);
1330        let cols = check_port_collisions(root).unwrap();
1331        // Both api_port (9000) and console_port (9001) collide.
1332        assert_eq!(cols.len(), 2, "{cols:?}");
1333        let ports: Vec<u16> = cols.iter().map(|c| c.port).collect();
1334        assert!(ports.contains(&9000));
1335        assert!(ports.contains(&9001));
1336    }
1337
1338    #[test]
1339    fn missing_services_dir_is_not_error_for_ports() {
1340        let dir = tempdir().unwrap();
1341        assert!(check_port_collisions(dir.path()).unwrap().is_empty());
1342    }
1343
1344    // ── R763: the retired `tier:` arch mesh tag ────────────────────────────
1345
1346    /// Writes and re-parses the TOML immediately: [`load_machine_tomls`]'s
1347    /// tolerant mode *skips* a file that fails to deserialize, so a fixture
1348    /// missing a required `MachineConfig` field would otherwise make every
1349    /// assert-empty test using it pass vacuously instead of failing loud.
1350    fn assert_machine_toml_parses(path: &Path) {
1351        let src = std::fs::read_to_string(path).unwrap();
1352        toml::from_str::<MachineConfig>(&src)
1353            .unwrap_or_else(|e| panic!("fixture {} does not parse as MachineConfig: {e}\n{src}", path.display()));
1354    }
1355
1356    fn write_machine_tags(workspace: &Path, name: &str, mesh_tags: &[&str]) {
1357        let dir = workspace.join(".yah/infra/machines");
1358        std::fs::create_dir_all(&dir).unwrap();
1359        let list: Vec<String> = mesh_tags.iter().map(|t| format!("\"{t}\"")).collect();
1360        let path = dir.join(format!("{name}.toml"));
1361        std::fs::write(
1362            &path,
1363            format!(
1364                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = [{}]\n",
1365                list.join(", ")
1366            ),
1367        )
1368        .unwrap();
1369        assert_machine_toml_parses(&path);
1370    }
1371
1372    #[test]
1373    fn the_current_arch_tag_is_clean() {
1374        let dir = tempdir().unwrap();
1375        write_machine_tags(dir.path(), "n1", &["tag:build-worker", "arch:x86", "os:linux"]);
1376        assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1377    }
1378
1379    #[test]
1380    fn a_stale_tier_arch_tag_is_flagged_with_its_replacement() {
1381        let dir = tempdir().unwrap();
1382        write_machine_tags(dir.path(), "n1", &["tag:build-worker", "tier:arm", "os:linux"]);
1383        let found = check_retired_arch_tags(dir.path()).unwrap();
1384        assert_eq!(found.len(), 1, "{found:?}");
1385        assert_eq!(found[0].tag, "tier:arm");
1386        assert_eq!(found[0].replacement(), "arch:arm");
1387        let msg = found[0].message();
1388        // The message has to carry the fix, not just the complaint — this lint
1389        // exists precisely because the symptom ("no node") names nothing.
1390        assert!(msg.contains("arch:arm"), "{msg}");
1391        assert!(msg.contains("n1"), "{msg}");
1392    }
1393
1394    /// The whole point: a node with the old tag is not *rejected* by placement,
1395    /// it silently stops being a candidate. Superset matching has no way to say
1396    /// "you asked for arch:x86 and I have tier:x86".
1397    #[test]
1398    fn a_stale_tag_is_not_caught_by_the_inert_taint_lint() {
1399        let dir = tempdir().unwrap();
1400        write_machine_tags(dir.path(), "n1", &["tier:x86"]);
1401        assert!(
1402            check_inert_taints(dir.path()).unwrap().is_empty(),
1403            "mesh tags are not taints — this needs its own check"
1404        );
1405        assert_eq!(check_retired_arch_tags(dir.path()).unwrap().len(), 1);
1406    }
1407
1408    #[test]
1409    fn missing_machines_dir_is_not_error_for_arch_tags() {
1410        let dir = tempdir().unwrap();
1411        assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1412    }
1413
1414    // ── R605-F12: sovereign members with no stated role ────────────────────
1415
1416    /// `stamp` is the sovereign declaration under test; everything else is the
1417    /// minimum a `MachineConfig` deserializes from. `assert_machine_toml_parses`
1418    /// catches a future edit here that drops a required field before it can
1419    /// produce a vacuously-passing assert-empty test (see that fn's doc).
1420    fn write_sovereign_machine(workspace: &Path, name: &str, stamp: &str) {
1421        let dir = workspace.join(".yah/infra/machines");
1422        std::fs::create_dir_all(&dir).unwrap();
1423        let path = dir.join(format!("{name}.toml"));
1424        std::fs::write(
1425            &path,
1426            format!("name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n{stamp}\n"),
1427        )
1428        .unwrap();
1429        assert_machine_toml_parses(&path);
1430    }
1431
1432    // ── R859-F2: the ingress floating IP declaration ──────────────────────
1433
1434    /// `provider` and the sovereign/floating-IP stamp are what vary; everything
1435    /// else is the minimum a `MachineConfig` deserializes from.
1436    fn write_fip_machine(workspace: &Path, name: &str, provider: &str, stamp: &str) {
1437        let dir = workspace.join(".yah/infra/machines");
1438        std::fs::create_dir_all(&dir).unwrap();
1439        let path = dir.join(format!("{name}.toml"));
1440        std::fs::write(
1441            &path,
1442            format!("name = \"{name}\"\nprovider = \"{provider}\"\nmesh_tags = []\n{stamp}\n"),
1443        )
1444        .unwrap();
1445        assert_machine_toml_parses(&path);
1446    }
1447
1448    /// The normal case, and it must be silent: most machines have no floating
1449    /// IP, and a lint that fires on every mesh-only box is one an operator
1450    /// learns to ignore.
1451    #[test]
1452    fn a_machine_with_no_ingress_floating_ip_is_never_flagged() {
1453        let dir = tempdir().unwrap();
1454        write_fip_machine(dir.path(), "us-west-002", "static", "");
1455        write_fip_machine(dir.path(), "n1", "digitalocean", "");
1456        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1457    }
1458
1459    #[test]
1460    fn a_valid_declaration_on_an_adapter_backed_provider_is_clean() {
1461        let dir = tempdir().unwrap();
1462        write_fip_machine(
1463            dir.path(),
1464            "us-west-001",
1465            "hetzner",
1466            "ingress_floating_ip = \"42\"",
1467        );
1468        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1469    }
1470
1471    /// The declaration that reads as a working failover path and is not one.
1472    #[test]
1473    fn a_provider_with_no_floating_ip_adapter_is_flagged_with_what_is_supported() {
1474        let dir = tempdir().unwrap();
1475        write_fip_machine(
1476            dir.path(),
1477            "us-east-001",
1478            "static",
1479            "ingress_floating_ip = \"51.81.85.200\"",
1480        );
1481        let found = check_ingress_floating_ip(dir.path()).unwrap();
1482        assert_eq!(found.len(), 1, "{found:?}");
1483        assert_eq!(
1484            found[0].kind,
1485            IngressFloatingIpProblemKind::NoAdapter {
1486                provider: "static".into()
1487            }
1488        );
1489        let msg = found[0].message();
1490        assert!(msg.contains("us-east-001"), "{msg}");
1491        assert!(msg.contains("hetzner"), "the message must name what IS supported: {msg}");
1492        assert!(msg.ends_with("us-east-001.toml"), "{msg}");
1493    }
1494
1495    /// An empty string is a declaration that resolves to nothing — distinct
1496    /// from omitting the key, which is the supported "no floating-IP path".
1497    #[test]
1498    fn a_blank_declaration_is_flagged_rather_than_read_as_absent() {
1499        let dir = tempdir().unwrap();
1500        write_fip_machine(
1501            dir.path(),
1502            "us-west-001",
1503            "hetzner",
1504            "ingress_floating_ip = \"  \"",
1505        );
1506        let found = check_ingress_floating_ip(dir.path()).unwrap();
1507        assert_eq!(found.len(), 1, "{found:?}");
1508        assert_eq!(found[0].kind, IngressFloatingIpProblemKind::Blank);
1509    }
1510
1511    /// The failure this check exists for: one IP moves between the boxes of a
1512    /// cohort, so two ids means a failover reassigns an IP that is not the one
1513    /// serving traffic — a flip that reports success and serves nothing.
1514    #[test]
1515    fn two_ids_in_one_sovereign_group_are_refused_and_name_both_machines() {
1516        let dir = tempdir().unwrap();
1517        write_fip_machine(
1518            dir.path(),
1519            "us-east-001",
1520            "hetzner",
1521            "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1522        );
1523        write_fip_machine(
1524            dir.path(),
1525            "us-west-001",
1526            "hetzner",
1527            "sovereign_group = \"prod\"\ningress_floating_ip = \"77\"",
1528        );
1529        let found = check_ingress_floating_ip(dir.path()).unwrap();
1530        assert_eq!(found.len(), 1, "{found:?}");
1531        let msg = found[0].message();
1532        assert!(msg.contains("us-west-001") && msg.contains("us-east-001"), "{msg}");
1533        assert!(msg.contains("42") && msg.contains("77"), "{msg}");
1534    }
1535
1536    #[test]
1537    fn one_id_across_a_whole_cohort_is_clean() {
1538        let dir = tempdir().unwrap();
1539        for name in ["us-east-001", "us-west-001", "us-south-001"] {
1540            write_fip_machine(
1541                dir.path(),
1542                name,
1543                "hetzner",
1544                "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1545            );
1546        }
1547        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1548    }
1549
1550    /// Two standalone boxes holding different IPs are two independent facts,
1551    /// not one contradiction — there is no cohort for them to disagree within.
1552    #[test]
1553    fn machines_in_no_group_may_hold_different_ips() {
1554        let dir = tempdir().unwrap();
1555        write_fip_machine(
1556            dir.path(),
1557            "us-east-001",
1558            "hetzner",
1559            "ingress_floating_ip = \"42\"",
1560        );
1561        write_fip_machine(
1562            dir.path(),
1563            "us-west-001",
1564            "vultr",
1565            "ingress_floating_ip = \"a-uuid\"",
1566        );
1567        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1568    }
1569
1570    #[test]
1571    fn missing_machines_dir_is_not_error_for_ingress_floating_ip() {
1572        let dir = tempdir().unwrap();
1573        assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1574    }
1575
1576    #[test]
1577    fn a_group_with_no_role_is_reported_with_the_declaring_file() {
1578        let dir = tempdir().unwrap();
1579        let root = dir.path();
1580        write_sovereign_machine(root, "us-west-001", "sovereign_group = \"prod\"");
1581        let found = check_unroled_sovereign_members(root).unwrap();
1582        assert_eq!(found.len(), 1, "{found:?}");
1583        assert_eq!(found[0].machine, "us-west-001");
1584        assert_eq!(found[0].group, "prod");
1585        assert!(found[0].machine_toml.ends_with("us-west-001.toml"));
1586        let msg = found[0].message();
1587        // The message has to say what the silence currently means, or the
1588        // operator reads it as pedantry and adds the line without deciding.
1589        assert!(msg.contains("voter") && msg.contains("non-voter"), "{msg}");
1590        assert!(msg.contains("TOP-LEVEL"), "{msg}");
1591    }
1592
1593    #[test]
1594    fn either_stated_role_is_clean() {
1595        let dir = tempdir().unwrap();
1596        let root = dir.path();
1597        write_sovereign_machine(
1598            root,
1599            "us-west-001",
1600            "sovereign_group = \"prod\"\nsovereign_role = \"voter\"",
1601        );
1602        write_sovereign_machine(
1603            root,
1604            "us-west-003",
1605            "sovereign_group = \"prod\"\nsovereign_role = \"non-voter\"",
1606        );
1607        assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1608    }
1609
1610    /// A standalone box has no quorum to be eligible for, so demanding a role
1611    /// of it would be noise — and noise is what trains an operator to stop
1612    /// reading the check that matters.
1613    #[test]
1614    fn a_machine_in_no_group_is_not_asked_for_a_role() {
1615        let dir = tempdir().unwrap();
1616        let root = dir.path();
1617        write_sovereign_machine(root, "us-west-002", "taints = [\"no-appliance\"]");
1618        assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1619    }
1620
1621    #[test]
1622    fn unroled_findings_are_ordered_by_file_so_output_is_stable() {
1623        let dir = tempdir().unwrap();
1624        let root = dir.path();
1625        write_sovereign_machine(root, "b-node", "sovereign_group = \"dev\"");
1626        write_sovereign_machine(root, "a-node", "sovereign_group = \"prod\"");
1627        let found = check_unroled_sovereign_members(root).unwrap();
1628        let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1629        assert_eq!(names, vec!["a-node", "b-node"]);
1630    }
1631
1632    #[test]
1633    fn missing_machines_dir_is_not_error_for_unroled_members() {
1634        let dir = tempdir().unwrap();
1635        assert!(check_unroled_sovereign_members(dir.path())
1636            .unwrap()
1637            .is_empty());
1638    }
1639
1640    // ── R605-T10: LAN dial targets ─────────────────────────────────────────
1641
1642    /// `connect` is the raw body of the `[connect]` table; `registration` the
1643    /// raw body of `[registration]` (empty string omits the table).
1644    fn write_reach_machine(workspace: &Path, name: &str, connect: &str, registration: &str) {
1645        let dir = workspace.join(".yah/infra/machines");
1646        std::fs::create_dir_all(&dir).unwrap();
1647        let path = dir.join(format!("{name}.toml"));
1648        let reg = if registration.is_empty() {
1649            String::new()
1650        } else {
1651            format!("\n[registration]\n{registration}\n")
1652        };
1653        std::fs::write(
1654            &path,
1655            format!(
1656                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n\n\
1657                 [connect]\n{connect}\n{reg}"
1658            ),
1659        )
1660        .unwrap();
1661        assert_machine_toml_parses(&path);
1662    }
1663
1664    #[test]
1665    fn a_lan_literal_in_the_dialed_field_is_reported_with_its_file() {
1666        let dir = tempdir().unwrap();
1667        let root = dir.path();
1668        // us-west-011's shape at filing time: LAN literal, no mesh address.
1669        write_reach_machine(
1670            root,
1671            "us-west-011",
1672            "address = \"192.168.10.11\"\nssh = \"yah@192.168.10.11\"\n\
1673             yubaba = \"http://192.168.10.11:7443\"",
1674            "",
1675        );
1676        let found = check_lan_dial_targets(root).unwrap();
1677        assert_eq!(found.len(), 1);
1678        assert_eq!(found[0].machine, "us-west-011");
1679        assert_eq!(found[0].url, "http://192.168.10.11:7443");
1680        assert_eq!(found[0].mesh_ipv4, None);
1681        let msg = found[0].message();
1682        assert!(msg.contains("mesh-join it"), "{msg}");
1683        assert!(msg.contains("us-west-011.toml"), "{msg}");
1684    }
1685
1686    /// A mesh-joined box gets the cheaper instruction, because the fix really
1687    /// is one deleted line — us-west-013/014's shape.
1688    #[test]
1689    fn a_mesh_joined_lan_declarer_is_told_to_delete_the_line() {
1690        let dir = tempdir().unwrap();
1691        let root = dir.path();
1692        write_reach_machine(
1693            root,
1694            "us-west-014",
1695            "address = \"192.168.10.14\"\nssh = \"yah@192.168.10.14\"\n\
1696             yubaba = \"http://192.168.10.14:7443\"",
1697            "mesh_ipv4 = \"100.64.0.6\"",
1698        );
1699        let found = check_lan_dial_targets(root).unwrap();
1700        assert_eq!(found.len(), 1);
1701        assert_eq!(found[0].mesh_ipv4.as_deref(), Some("100.64.0.6"));
1702        let msg = found[0].message();
1703        assert!(msg.contains("DELETE the `yubaba` line"), "{msg}");
1704        assert!(msg.contains("100.64.0.6"), "{msg}");
1705    }
1706
1707    /// The three shapes that must NOT be flagged: a mesh address, the pre-mesh
1708    /// loopback placeholder, and a LAN address confined to the metadata fields
1709    /// (which is the whole point — the operator keeps it, automation ignores it).
1710    #[test]
1711    fn mesh_loopback_and_metadata_only_lan_addresses_are_clean() {
1712        let dir = tempdir().unwrap();
1713        let root = dir.path();
1714        write_reach_machine(
1715            root,
1716            "meshed",
1717            "address = \"192.168.10.15\"\nssh = \"yah@192.168.10.15\"",
1718            "mesh_ipv4 = \"100.64.0.7\"",
1719        );
1720        write_reach_machine(
1721            root,
1722            "tunnelled",
1723            "address = \"192.168.10.16\"\nssh = \"yah@192.168.10.16\"\n\
1724             yubaba = \"http://127.0.0.1:7443\"",
1725            "",
1726        );
1727        write_reach_machine(
1728            root,
1729            "public",
1730            "address = \"45.32.194.254\"\nssh = \"debian@45.32.194.254\"\n\
1731             yubaba = \"http://45.32.194.254:7443\"",
1732            "",
1733        );
1734        assert!(check_lan_dial_targets(root).unwrap().is_empty());
1735    }
1736
1737    #[test]
1738    fn missing_machines_dir_is_not_error_for_lan_dial_targets() {
1739        let dir = tempdir().unwrap();
1740        assert!(check_lan_dial_targets(dir.path()).unwrap().is_empty());
1741    }
1742
1743    // ── R742-T4: inert taints ──────────────────────────────────────────────
1744
1745    fn write_machine(workspace: &Path, name: &str, taints: &[&str]) {
1746        let dir = workspace.join(".yah/infra/machines");
1747        std::fs::create_dir_all(&dir).unwrap();
1748        let list: Vec<String> = taints.iter().map(|t| format!("\"{t}\"")).collect();
1749        let path = dir.join(format!("{name}.toml"));
1750        std::fs::write(
1751            &path,
1752            format!(
1753                "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\ntaints = [{}]\n",
1754                list.join(", ")
1755            ),
1756        )
1757        .unwrap();
1758        assert_machine_toml_parses(&path);
1759    }
1760
1761    #[test]
1762    fn archetype_repel_keys_and_affinity_keys_are_clean() {
1763        let dir = tempdir().unwrap();
1764        let root = dir.path();
1765        write_machine(root, "worker", &["no-server", "no-appliance", "no-job"]);
1766        write_machine(root, "edge", &["public-ip"]);
1767        write_machine(root, "plain", &[]);
1768        assert!(check_inert_taints(root).unwrap().is_empty());
1769    }
1770
1771    #[test]
1772    fn a_free_form_taint_is_reported_with_the_declaring_file() {
1773        let dir = tempdir().unwrap();
1774        let root = dir.path();
1775        // W305's headline example. Environment is not a taint.
1776        write_machine(root, "us-west-011", &["qa"]);
1777        let found = check_inert_taints(root).unwrap();
1778        assert_eq!(found.len(), 1, "{found:?}");
1779        assert_eq!(found[0].machine, "us-west-011");
1780        assert_eq!(found[0].key, "qa");
1781        assert!(found[0].machine_toml.ends_with("us-west-011.toml"));
1782        let msg = found[0].message();
1783        // The message has to carry the legal vocabulary, otherwise the
1784        // operator's only recourse is reading the scheduler.
1785        assert!(msg.contains("no-appliance"), "{msg}");
1786        assert!(msg.contains("public-ip"), "{msg}");
1787        assert!(msg.contains("mesh_tags"), "{msg}");
1788    }
1789
1790    #[test]
1791    fn no_voter_is_inert_because_voter_is_not_an_archetype() {
1792        // The one that cost real fleet state: it reads as an exclusion and
1793        // excludes nothing, so it sat on three nodes asserting a falsehood.
1794        let dir = tempdir().unwrap();
1795        let root = dir.path();
1796        write_machine(root, "us-west-015", &["no-server", "no-appliance", "no-voter"]);
1797        let found = check_inert_taints(root).unwrap();
1798        assert_eq!(found.len(), 1, "{found:?}");
1799        assert_eq!(found[0].key, "no-voter");
1800    }
1801
1802    #[test]
1803    fn findings_are_ordered_by_file_so_output_is_stable() {
1804        let dir = tempdir().unwrap();
1805        let root = dir.path();
1806        write_machine(root, "b-node", &["qa"]);
1807        write_machine(root, "a-node", &["staging"]);
1808        let found = check_inert_taints(root).unwrap();
1809        let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1810        assert_eq!(names, vec!["a-node", "b-node"]);
1811    }
1812
1813    #[test]
1814    fn missing_machines_dir_is_not_error() {
1815        let dir = tempdir().unwrap();
1816        assert!(check_inert_taints(dir.path()).unwrap().is_empty());
1817    }
1818
1819    #[test]
1820    fn an_unparseable_machine_toml_is_skipped_not_fatal() {
1821        let dir = tempdir().unwrap();
1822        let root = dir.path();
1823        let mdir = root.join(".yah/infra/machines");
1824        std::fs::create_dir_all(&mdir).unwrap();
1825        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1826        write_machine(root, "good", &["qa"]);
1827        // The broken file must not sink the sweep — a peer's half-written
1828        // scaffold is a normal state on a shared tree.
1829        let found = check_inert_taints(root).unwrap();
1830        assert_eq!(found.len(), 1);
1831        assert_eq!(found[0].machine, "good");
1832    }
1833
1834    // ── R787: the shared loader's Strict/Tolerant contract ──────────────────
1835
1836    #[test]
1837    fn tolerant_mode_skips_an_unparseable_toml_and_still_pairs_the_path() {
1838        let dir = tempdir().unwrap();
1839        let root = dir.path();
1840        let mdir = root.join(".yah/infra/machines");
1841        std::fs::create_dir_all(&mdir).unwrap();
1842        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1843        write_machine(root, "good", &["qa"]);
1844
1845        let loaded = load_machine_tomls(root, MachineLoadMode::Tolerant).unwrap();
1846        assert_eq!(loaded.len(), 1, "{loaded:?}");
1847        assert_eq!(loaded[0].1.name, "good");
1848        assert!(loaded[0].0.ends_with("good.toml"));
1849    }
1850
1851    #[test]
1852    fn strict_mode_fails_the_whole_load_on_one_unparseable_toml() {
1853        // The behavior collate_workspace_ingress relies on: a bad machine toml
1854        // must not silently resolve a `required` placement onto the wrong node.
1855        let dir = tempdir().unwrap();
1856        let root = dir.path();
1857        let mdir = root.join(".yah/infra/machines");
1858        std::fs::create_dir_all(&mdir).unwrap();
1859        std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1860        write_machine(root, "good", &["qa"]);
1861
1862        let err = load_machine_tomls(root, MachineLoadMode::Strict).unwrap_err();
1863        assert!(format!("{err:#}").contains("broken.toml"), "{err:#}");
1864    }
1865
1866    // ── R742-F2 (W305): workspace ingress collation ──
1867
1868    /// A mirror fronting one hostname through one edge on `machines`.
1869    fn fronted_mirror(provider: &str, machines: &[&str], hostname: &str, port: u16) -> String {
1870        let list = machines
1871            .iter()
1872            .map(|m| format!("\"{m}\""))
1873            .collect::<Vec<_>>()
1874            .join(", ");
1875        format!(
1876            "schema_version = 1\nshape = \"single-machine\"\n\
1877             ingress = \"{provider}\"\ningress_machines = [{list}]\n\
1878             [providers.compute]\nuse = \"hetzner\"\nzone = \"{hostname}\"\n\
1879             port = {port}\nupstream_host = \"100.64.0.5\"\n"
1880        )
1881    }
1882
1883    #[test]
1884    fn two_services_fronting_one_node_collate_into_one_front_door() {
1885        let dir = tempdir().unwrap();
1886        let root = dir.path();
1887        write_service(root, "yah-marketing", "yah-marketing/site");
1888        write_mirror(
1889            root,
1890            "yah-marketing",
1891            "cloud",
1892            &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1893        );
1894        write_service(root, "yah-issues", "yah-issues/site");
1895        write_mirror(
1896            root,
1897            "yah-issues",
1898            "cloud",
1899            &fronted_mirror("passway", &["us-east-001"], "issues.yah.dev", 8731),
1900        );
1901
1902        let report = collate_workspace_ingress(root).unwrap();
1903        assert!(report.problems.is_empty(), "{:?}", report.problems);
1904        assert_eq!(report.collation.front_doors.len(), 1);
1905        let door = &report.collation.front_doors[0];
1906        assert_eq!(door.machine, "us-east-001");
1907        assert_eq!(
1908            door.passway_upstreams().unwrap(),
1909            vec!["issues.yah.dev=100.64.0.5:8731", "yah.dev=100.64.0.5:8080"]
1910        );
1911    }
1912
1913    #[test]
1914    fn a_cross_service_hostname_clash_is_reported_with_both_declarations() {
1915        // Neither service's own `yah cloud apply` can see this: each plans its
1916        // own mirror, both look fine, and the box ends up with whichever
1917        // applied last.
1918        let dir = tempdir().unwrap();
1919        let root = dir.path();
1920        write_service(root, "svc-a", "svc-a/site");
1921        write_mirror(
1922            root,
1923            "svc-a",
1924            "cloud",
1925            &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1926        );
1927        write_service(root, "svc-b", "svc-b/site");
1928        write_mirror(
1929            root,
1930            "svc-b",
1931            "cloud",
1932            &fronted_mirror("cloudflare-tunnel", &["us-west-001"], "yah.dev", 8080),
1933        );
1934
1935        let report = collate_workspace_ingress(root).unwrap();
1936        let fatal: Vec<String> = report
1937            .problems
1938            .iter()
1939            .filter(|p| p.is_fatal())
1940            .map(|p| p.message())
1941            .collect();
1942        assert_eq!(fatal.len(), 1, "{fatal:?}");
1943        assert!(fatal[0].contains("svc-a/cloud"), "{}", fatal[0]);
1944        assert!(fatal[0].contains("svc-b/cloud"), "{}", fatal[0]);
1945    }
1946
1947    #[test]
1948    fn one_mirrors_broken_declaration_does_not_hide_the_rest() {
1949        // A malformed edge is reported against the file that declared it, and
1950        // the sweep continues — one service's typo must not blind the operator
1951        // to every other service's front doors.
1952        let dir = tempdir().unwrap();
1953        let root = dir.path();
1954        write_service(root, "svc-broken", "svc-broken/site");
1955        write_mirror(
1956            root,
1957            "svc-broken",
1958            "cloud",
1959            "schema_version = 1\nshape = \"single-machine\"\n\
1960             ingress_machines = [\"us-east-001\"]\n\
1961             [providers.compute]\nuse = \"hetzner\"\nzone = \"a.yah.dev\"\nport = 8080\n",
1962        );
1963        write_service(root, "svc-ok", "svc-ok/site");
1964        write_mirror(
1965            root,
1966            "svc-ok",
1967            "cloud",
1968            &fronted_mirror("passway", &["us-east-001"], "b.yah.dev", 8080),
1969        );
1970
1971        let report = collate_workspace_ingress(root).unwrap();
1972        assert_eq!(report.problems.len(), 1);
1973        assert!(
1974            report.problems[0].message().contains("svc-broken/cloud"),
1975            "{}",
1976            report.problems[0].message()
1977        );
1978        assert_eq!(report.collation.front_doors.len(), 1, "svc-ok still collates");
1979    }
1980
1981    #[test]
1982    fn a_mirror_with_no_ingress_contributes_nothing_and_is_not_a_finding() {
1983        let dir = tempdir().unwrap();
1984        let root = dir.path();
1985        write_service(root, "svc", "svc/site");
1986        write_mirror(root, "svc", "dev", &local_static_mirror(4322));
1987        let report = collate_workspace_ingress(root).unwrap();
1988        assert!(report.problems.is_empty());
1989        assert!(report.collation.front_doors.is_empty());
1990    }
1991}