Skip to main content

rch_common/errors/
explain.rs

1//! Operator-facing code-lookup surface (`rch error explain`).
2//!
3//! Bridges the two parallel code namespaces in this workspace —
4//! [`super::ErrorCode`] (`RCH-E001..E599`, the primary error catalog)
5//! and [`super::ReliabilityReasonCode`] (`RCH-R001..R699`, the doctor
6//! reliability surface) — into a single uniform lookup so an operator
7//! who pastes any code from a log line into `rch error explain` gets a
8//! useful answer regardless of which namespace it came from.
9//!
10//! # Surface
11//!
12//! ```text
13//! rch error explain <CODE>      # human form
14//! rch error explain <CODE> --json
15//! rch error list                # all codes
16//! rch error list --category=worker
17//! rch error list --json
18//! ```
19//!
20//! # Wire shape (JSON)
21//!
22//! ```json
23//! {
24//!   "code": "RCH-R104",
25//!   "namespace": "reliability",
26//!   "name": "WorkerDiskPressureTelemetryGap",
27//!   "category": "disk_pressure",
28//!   "description": "Worker is missing fresh disk telemetry.",
29//!   "remediation": ["Telemetry refresh is automatic; wait for the next poll, or run `rch daemon restart` to force a fresh poll cycle."],
30//!   "requires_restart": false
31//! }
32//! ```
33
34use super::{ErrorCategory, ErrorCode, ReliabilityCategoryKind, ReliabilityReasonCode};
35use crate::incident::IncidentReasonCode;
36use serde::{Deserialize, Serialize};
37
38/// Resolved lookup result for either namespace. Produced by [`lookup`].
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CodeExplanation {
41    pub code: String,
42    pub namespace: CodeNamespace,
43    pub name: String,
44    pub category: String,
45    pub description: String,
46    pub remediation: Vec<String>,
47    /// Only meaningful for reliability codes. `None` for error codes
48    /// where the concept doesn't apply.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub requires_restart: Option<bool>,
51    /// Optional documentation link. Currently only populated for some
52    /// `ErrorCode` entries that supply a `doc_url`.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub doc_url: Option<String>,
55}
56
57/// Which catalog the code came from.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum CodeNamespace {
61    /// `RCH-Ennn` codes from [`ErrorCode`].
62    Error,
63    /// `RCH-Rnnn` codes from [`ReliabilityReasonCode`].
64    Reliability,
65    /// `RCH-Innn` incident/refusal reason codes from [`IncidentReasonCode`].
66    Incident,
67}
68
69/// Look up a code string. Accepts whitespace-padded input.
70///
71/// Returns `None` if the code is unknown or malformed.
72#[must_use]
73pub fn lookup(raw: &str) -> Option<CodeExplanation> {
74    let normalized = raw.trim().to_ascii_uppercase();
75    if let Some(c) = lookup_reliability(&normalized) {
76        return Some(c);
77    }
78    if let Some(c) = lookup_error(&normalized) {
79        return Some(c);
80    }
81    if let Some(c) = lookup_incident(&normalized) {
82        return Some(c);
83    }
84    None
85}
86
87/// Whether a string parses as either a known reliability or error code.
88#[must_use]
89pub fn is_known(raw: &str) -> bool {
90    lookup(raw).is_some()
91}
92
93fn lookup_reliability(code: &str) -> Option<CodeExplanation> {
94    let v = ReliabilityReasonCode::from_code_str(code)?;
95    Some(CodeExplanation {
96        code: v.code().to_string(),
97        namespace: CodeNamespace::Reliability,
98        name: v.name().to_string(),
99        category: reliability_category_str(v.category()).to_string(),
100        description: v.remediation_hint().to_string(),
101        remediation: vec![v.remediation_hint().to_string()],
102        requires_restart: Some(v.requires_restart()),
103        doc_url: None,
104    })
105}
106
107fn lookup_error(code: &str) -> Option<CodeExplanation> {
108    for c in error_code_all() {
109        if c.code_string() == code {
110            let entry = c.entry();
111            return Some(CodeExplanation {
112                code: entry.code,
113                namespace: CodeNamespace::Error,
114                name: format!("{c:?}"),
115                category: error_category_str(entry.category).to_string(),
116                description: entry.message,
117                remediation: entry.remediation,
118                requires_restart: None,
119                doc_url: entry.doc_url,
120            });
121        }
122    }
123    None
124}
125
126/// Resolve an `RCH-Innn` incident/refusal reason code. The incident registry
127/// ([`IncidentReasonCode`]) owns the canonical code list and operator-facing
128/// failure-class strings; the description/category/remediation are authored
129/// here (the codes are operational/policy reasons, several of which have no
130/// single error-catalog analogue).
131fn lookup_incident(code: &str) -> Option<CodeExplanation> {
132    let v = IncidentReasonCode::from_code_str(code)?;
133    Some(CodeExplanation {
134        code: v.code().to_string(),
135        namespace: CodeNamespace::Incident,
136        name: format!("{v:?}"),
137        category: incident_category(v).to_string(),
138        description: incident_description(v).to_string(),
139        remediation: incident_remediation(v),
140        requires_restart: None,
141        doc_url: None,
142    })
143}
144
145/// Snake-case category for an incident reason code (aligns with the reliability
146/// failure taxonomy where it overlaps).
147const fn incident_category(v: IncidentReasonCode) -> &'static str {
148    use IncidentReasonCode as I;
149    match v {
150        I::NoAdmissibleWorkers | I::HardPreflight => "admission",
151        I::CriticalPressure | I::DiskFull => "disk_pressure",
152        I::InsufficientSlots | I::QueueAmbiguity => "capacity",
153        I::ActiveProjectExclusion => "path_deps",
154        I::MissingRuntimeToolchainTarget => "capability",
155        I::OsArchMismatch | I::WrongUserPathWorkerBinary => "worker_binary",
156        I::TelemetryStale => "telemetry",
157        I::CircuitOpen => "worker",
158        I::DaemonSocketRefused => "daemon",
159        I::LocalFallback => "fallback",
160        I::ProofRefusal => "proof",
161        I::RsyncVanishedFile | I::ArtifactMiss => "transfer",
162        I::ToolchainDrift => "self_test",
163    }
164}
165
166/// One-line operator-facing description for an incident reason code.
167const fn incident_description(v: IncidentReasonCode) -> &'static str {
168    use IncidentReasonCode as I;
169    match v {
170        I::NoAdmissibleWorkers => "No worker passed admission — every candidate was rejected.",
171        I::CriticalPressure => {
172            "A worker (or the pool) was under critical disk/memory pressure and could not admit work."
173        }
174        I::InsufficientSlots => "Not enough free build slots to admit the request right now.",
175        I::HardPreflight => "A hard preflight check rejected the only candidate worker(s).",
176        I::ActiveProjectExclusion => {
177            "The active project root was excluded from offload by a path-dependency or topology rule."
178        }
179        I::MissingRuntimeToolchainTarget => {
180            "A required runtime, toolchain, or Rust target was missing on the worker(s)."
181        }
182        I::OsArchMismatch => {
183            "The worker's OS/arch did not match the required artifact/target triple."
184        }
185        I::TelemetryStale => "Worker telemetry was stale or its age could not be determined.",
186        I::CircuitOpen => "The worker's circuit breaker was open (repeated failures isolated it).",
187        I::DaemonSocketRefused => {
188            "The daemon Unix socket refused or could not accept the connection."
189        }
190        I::LocalFallback => {
191            "The build fell back to local execution (fail-open) instead of offloading."
192        }
193        I::ProofRefusal => {
194            "Proof mode (RCH_REQUIRE_REMOTE) refused to proceed because remote execution could not be guaranteed."
195        }
196        I::RsyncVanishedFile => "rsync reported a source file that vanished mid-transfer.",
197        I::ArtifactMiss => "An expected build artifact was missing on retrieval.",
198        I::QueueAmbiguity => "A build's local/remote job identity could not be correlated cleanly.",
199        I::DiskFull => "The target disk was full.",
200        I::WrongUserPathWorkerBinary => {
201            "A wrong-user or wrong-path/arch rch-wkr binary was detected on the worker."
202        }
203        I::ToolchainDrift => {
204            "A self-test canary built successfully but its bytes differ from the orchestrator's reference because the worker's toolchain differs — a healthy worker, not a failure."
205        }
206    }
207}
208
209/// Authored remediation steps for an incident reason code. These match the
210/// next-actions surfaced by `rch admit` and the canonical rch skill's RCH-Innn
211/// table, so the explainer and the live surfaces agree.
212fn incident_remediation(v: IncidentReasonCode) -> Vec<String> {
213    use IncidentReasonCode as I;
214    let steps: &[&str] = match v {
215        I::NoAdmissibleWorkers => &[
216            "Run `rch status --fleet` to see whether the fleet is absent, overloaded, or missing a capability.",
217            "Run `rch admit \"<command>\"` for the per-candidate rejection reasons.",
218        ],
219        I::CriticalPressure => &[
220            "Inspect pressure with `rch doctor --reliability --scope pressure`.",
221            "Reclaim space with `rch cache clean --older <dur> --execute` (the daemon's reaper protects active builds).",
222        ],
223        I::InsufficientSlots => &[
224            "Queue instead of falling back: keep `RCH_QUEUE_WHEN_BUSY=1` (the default).",
225            "Watch `rch queue`; add workers only if the fleet is genuinely under-provisioned.",
226        ],
227        I::HardPreflight => &[
228            "Run `rch admit \"<command>\"` to see which preflight check failed.",
229            "Fix the named capability/topology issue, or target another worker with `RCH_WORKER`.",
230        ],
231        I::ActiveProjectExclusion => &[
232            "Inspect the resolved plan: `rch diagnose \"<command>\" --json` (data.placement).",
233            "Ensure required sibling repos exist under the canonical project root on the worker.",
234        ],
235        I::MissingRuntimeToolchainTarget => &[
236            "Refresh worker facts: `rch workers capabilities --refresh`.",
237            "Install the missing toolchain/target on the worker, or `rch fleet deploy --worker <id> --force`.",
238        ],
239        I::OsArchMismatch => &[
240            "Re-deploy the correct target triple: `rch fleet deploy --worker <id> --force --verify`.",
241            "Confirm the needed target with `rch workers capabilities --refresh`.",
242        ],
243        I::TelemetryStale => &[
244            "Inspect freshness via `rch status --remediation` (telemetry band).",
245            "Usually self-heals on the next probe; check host distance/poll interval if it persists.",
246        ],
247        I::CircuitOpen => &[
248            "The breaker self-heals (open, half-open, closed) after a good probe — fix the worker, then `rch workers probe <id>`.",
249            "Do not `rch workers disable` a transiently-open worker; it auto-rejoins.",
250        ],
251        I::DaemonSocketRefused => &[
252            "Restart the daemon (it reclaims a stale socket): `rch daemon restart`.",
253            "Never `rm` the socket by hand; `rch doctor --fix` repairs common wiring.",
254        ],
255        I::LocalFallback => &[
256            "Expected fail-open behavior when remote is not worth it or unavailable — no action usually needed.",
257            "Force offload with `RCH_FORCE_REMOTE=1`, or forbid local fallback with `RCH_REQUIRE_REMOTE=1` (proof mode).",
258        ],
259        I::ProofRefusal => &[
260            "Proof mode is fail-closed by design — fix the underlying remote issue rather than retrying locally.",
261            "The proof intent is recorded and replays when capacity returns; inspect `rch status --remediation --json`.",
262            "Keep the command as direct argv after `--`; shell-wrapped commands are refused (RCH-E301).",
263        ],
264        I::RsyncVanishedFile => &[
265            "Usually transient (a file changed mid-transfer) — retry the build.",
266            "If persistent, exclude churny build dirs via `[transfer] exclude_patterns`.",
267        ],
268        I::ArtifactMiss => &[
269            "Re-run the build; inspect the remote target dir and artifact patterns with `rch diagnose \"<command>\" --json`.",
270            "Ensure the worker has disk headroom: `rch doctor --reliability --scope pressure`.",
271        ],
272        I::QueueAmbiguity => &[
273            "Inspect active/queued builds and their ids with `rch queue`.",
274            "Clear a wedged build with `rch cancel <id>`; the daemon reattaches recoverable jobs on restart.",
275        ],
276        I::DiskFull => &[
277            "Reclaim worker disk: `rch cache clean --older <dur> --execute` (the reaper protects active builds).",
278            "Inspect with `rch doctor --reliability --scope pressure`.",
279        ],
280        I::WrongUserPathWorkerBinary => &[
281            "Re-deploy atomically: `rch fleet deploy --worker <id> --force --verify` — do not hand-patch the worker.",
282            "Verify with `rch fleet verify --worker <id>`.",
283        ],
284        I::ToolchainDrift => &[
285            "Advisory only — the worker is healthy; its `rustc` nightly merely differs from the orchestrator's, so codegen differs. No action required.",
286            "To silence the advisory, align toolchains: `rch fleet deploy --worker <id>` after matching the worker's `rust-toolchain`, or compare with `rch workers capabilities --refresh`.",
287        ],
288    };
289    steps.iter().map(|s| (*s).to_string()).collect()
290}
291
292/// Snake-case string name for a reliability category.
293const fn reliability_category_str(c: ReliabilityCategoryKind) -> &'static str {
294    match c {
295        ReliabilityCategoryKind::Topology => "topology",
296        ReliabilityCategoryKind::DiskPressure => "disk_pressure",
297        ReliabilityCategoryKind::ProcessTriage => "process_triage",
298        ReliabilityCategoryKind::RepoConvergence => "repo_convergence",
299        ReliabilityCategoryKind::HelperCompatibility => "helper_compatibility",
300        ReliabilityCategoryKind::RolloutPosture => "rollout_posture",
301        ReliabilityCategoryKind::SchemaCompatibility => "schema_compatibility",
302        ReliabilityCategoryKind::MirrorOwnership => "mirror_ownership",
303    }
304}
305
306/// Snake-case string name for an error category.
307const fn error_category_str(c: ErrorCategory) -> &'static str {
308    match c {
309        ErrorCategory::Config => "config",
310        ErrorCategory::Network => "network",
311        ErrorCategory::Worker => "worker",
312        ErrorCategory::Build => "build",
313        ErrorCategory::Transfer => "transfer",
314        ErrorCategory::Internal => "internal",
315    }
316}
317
318/// All known codes across both namespaces. Used by `rch error list`.
319#[must_use]
320pub fn list_all() -> Vec<CodeExplanation> {
321    let mut out: Vec<CodeExplanation> = Vec::new();
322    for v in ReliabilityReasonCode::ALL {
323        if let Some(e) = lookup_reliability(v.code()) {
324            out.push(e);
325        }
326    }
327    for c in error_code_all() {
328        let s = c.code_string();
329        if let Some(e) = lookup_error(&s) {
330            out.push(e);
331        }
332    }
333    for v in IncidentReasonCode::ALL {
334        if let Some(e) = lookup_incident(v.code()) {
335            out.push(e);
336        }
337    }
338    out.sort_by(|a, b| a.code.cmp(&b.code));
339    out
340}
341
342/// Subset of [`list_all`] filtered to one category (matches the
343/// snake_case `category` field on [`CodeExplanation`]). Empty result
344/// indicates an unknown category — caller can detect that.
345#[must_use]
346pub fn list_by_category(category: &str) -> Vec<CodeExplanation> {
347    let cat = category.trim().to_ascii_lowercase();
348    list_all()
349        .into_iter()
350        .filter(|e| e.category == cat)
351        .collect()
352}
353
354/// Known category names across both code namespaces, sorted for stable CLI
355/// help and JSON error payloads.
356#[must_use]
357pub fn known_categories() -> Vec<String> {
358    let mut categories: Vec<String> = list_all().into_iter().map(|e| e.category).collect();
359    categories.sort();
360    categories.dedup();
361    categories
362}
363
364/// Whether a category name matches at least one known code category.
365#[must_use]
366pub fn is_known_category(category: &str) -> bool {
367    let cat = category.trim().to_ascii_lowercase();
368    !cat.is_empty() && known_categories().iter().any(|known| known == &cat)
369}
370
371/// All known [`ErrorCode`] variants from the authoritative catalog.
372fn error_code_all() -> &'static [ErrorCode] {
373    ErrorCode::all()
374}
375
376/// Render a [`CodeExplanation`] in a paste-ready human form. Used by
377/// the CLI when `--json` is not set.
378#[must_use]
379pub fn render_human(e: &CodeExplanation) -> String {
380    let mut out = String::with_capacity(512);
381    out.push_str(&format!("{}  {}\n", e.code, e.name));
382    out.push_str(&format!(
383        "Category:     {:<30}  Namespace: {}\n",
384        e.category,
385        match e.namespace {
386            CodeNamespace::Error => "error (RCH-Ennn)",
387            CodeNamespace::Reliability => "reliability (RCH-Rnnn)",
388            CodeNamespace::Incident => "incident (RCH-Innn)",
389        }
390    ));
391    if let Some(rr) = e.requires_restart {
392        out.push_str(&format!("Requires restart: {rr}\n"));
393    }
394    out.push_str("\nDescription:\n");
395    out.push_str(&format!("  {}\n", e.description));
396    if !e.remediation.is_empty() {
397        out.push_str("\nRemediation:\n");
398        for step in &e.remediation {
399            out.push_str(&format!("  - {step}\n"));
400        }
401    }
402    if let Some(url) = &e.doc_url {
403        out.push_str(&format!("\nReference: {url}\n"));
404    }
405    out
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_lookup_reliability_code() {
414        let e = lookup("RCH-R104").expect("R104 known");
415        assert_eq!(e.code, "RCH-R104");
416        assert_eq!(e.namespace, CodeNamespace::Reliability);
417        assert_eq!(e.category, "disk_pressure");
418        assert!(!e.description.is_empty());
419        assert!(!e.remediation.is_empty());
420        assert!(e.requires_restart.is_some());
421    }
422
423    #[test]
424    fn test_lookup_error_code() {
425        let e = lookup("RCH-E001").expect("E001 known");
426        assert_eq!(e.code, "RCH-E001");
427        assert_eq!(e.namespace, CodeNamespace::Error);
428        assert_eq!(e.category, "config");
429        assert!(!e.description.is_empty());
430        assert!(e.requires_restart.is_none());
431    }
432
433    #[test]
434    fn test_lookup_dependency_preflight_error_code() {
435        let e = lookup("RCH-E410").expect("dependency preflight missing known");
436        assert_eq!(e.code, "RCH-E410");
437        assert_eq!(e.namespace, CodeNamespace::Error);
438        assert_eq!(e.name, "DependencyPreflightMissing");
439        assert_eq!(e.category, "transfer");
440        assert!(e.description.contains("missing required path"));
441        assert!(
442            e.remediation
443                .iter()
444                .any(|step| step.contains("missing remote path"))
445        );
446    }
447
448    #[test]
449    fn test_lookup_unknown_returns_none() {
450        assert!(lookup("RCH-R999").is_none());
451        assert!(lookup("RCH-E999").is_none());
452        assert!(lookup("not-a-code").is_none());
453        assert!(lookup("").is_none());
454    }
455
456    #[test]
457    fn test_lookup_trims_whitespace() {
458        let e = lookup("  RCH-R001  ").expect("trimmed lookup hits");
459        assert_eq!(e.code, "RCH-R001");
460    }
461
462    #[test]
463    fn test_lookup_is_case_insensitive() {
464        let e = lookup("rch-e001").expect("lowercase error-code lookup hits");
465        assert_eq!(e.code, "RCH-E001");
466
467        let e = lookup("rch-r104").expect("lowercase reliability-code lookup hits");
468        assert_eq!(e.code, "RCH-R104");
469    }
470
471    #[test]
472    fn test_is_known() {
473        assert!(is_known("RCH-R001"));
474        assert!(is_known("RCH-E001"));
475        assert!(!is_known("RCH-X001"));
476    }
477
478    #[test]
479    fn test_list_all_includes_both_namespaces() {
480        let all = list_all();
481        let reliability_count = all
482            .iter()
483            .filter(|e| e.namespace == CodeNamespace::Reliability)
484            .count();
485        let error_count = all
486            .iter()
487            .filter(|e| e.namespace == CodeNamespace::Error)
488            .count();
489        assert!(reliability_count >= 40, "expected ≥40 reliability codes");
490        assert!(error_count >= 50, "expected ≥50 error codes");
491    }
492
493    #[test]
494    fn test_list_all_sorted_by_code() {
495        let all = list_all();
496        for w in all.windows(2) {
497            assert!(w[0].code <= w[1].code, "list_all not sorted by code");
498        }
499    }
500
501    #[test]
502    fn test_list_all_no_duplicates() {
503        use std::collections::HashSet;
504        let all = list_all();
505        let codes: HashSet<&str> = all.iter().map(|e| e.code.as_str()).collect();
506        assert_eq!(codes.len(), all.len(), "duplicate code in list_all");
507    }
508
509    #[test]
510    fn test_list_by_category_filters() {
511        let dp = list_by_category("disk_pressure");
512        assert!(!dp.is_empty());
513        for e in &dp {
514            assert_eq!(e.category, "disk_pressure");
515        }
516    }
517
518    #[test]
519    fn test_list_by_category_unknown_returns_empty() {
520        assert!(list_by_category("nonexistent_category").is_empty());
521    }
522
523    #[test]
524    fn test_known_categories_are_sorted_unique_and_complete() {
525        let categories = known_categories();
526        assert!(!categories.is_empty());
527        for w in categories.windows(2) {
528            assert!(w[0] < w[1], "known_categories must be sorted and unique");
529        }
530        assert!(categories.iter().any(|c| c == "disk_pressure"));
531        assert!(categories.iter().any(|c| c == "worker"));
532        assert!(categories.iter().any(|c| c == "topology"));
533    }
534
535    #[test]
536    fn test_is_known_category_trims_and_normalizes_case() {
537        assert!(is_known_category(" disk_pressure "));
538        assert!(is_known_category("WORKER"));
539        assert!(is_known_category("Topology"));
540        assert!(!is_known_category(""));
541        assert!(!is_known_category("nonexistent_category"));
542    }
543
544    #[test]
545    fn test_list_by_category_case_insensitive() {
546        let lower = list_by_category("topology");
547        let upper = list_by_category("TOPOLOGY");
548        let mixed = list_by_category("Topology");
549        assert_eq!(lower.len(), upper.len());
550        assert_eq!(lower.len(), mixed.len());
551        assert!(!lower.is_empty());
552    }
553
554    #[test]
555    fn test_render_human_includes_code_and_name() {
556        let e = lookup("RCH-R104").unwrap();
557        let rendered = render_human(&e);
558        assert!(rendered.contains("RCH-R104"));
559        assert!(rendered.contains(&e.name));
560        assert!(rendered.contains("Description:"));
561        assert!(rendered.contains("Remediation:"));
562    }
563
564    #[test]
565    fn test_render_human_omits_requires_restart_for_error_codes() {
566        let e = lookup("RCH-E001").unwrap();
567        let rendered = render_human(&e);
568        assert!(!rendered.contains("Requires restart"));
569    }
570
571    #[test]
572    fn test_serde_roundtrip() {
573        let e = lookup("RCH-R001").unwrap();
574        let json = serde_json::to_string(&e).unwrap();
575        let back: CodeExplanation = serde_json::from_str(&json).unwrap();
576        assert_eq!(e.code, back.code);
577        assert_eq!(e.namespace, back.namespace);
578        assert_eq!(e.category, back.category);
579    }
580
581    #[test]
582    fn test_error_code_all_consistent_with_code_string() {
583        // Every code in the hand-maintained list MUST have a unique
584        // RCH-Ennn code_string. Asserts the catalog isn't drifting.
585        use std::collections::HashSet;
586        let codes: HashSet<String> = error_code_all().iter().map(|c| c.code_string()).collect();
587        assert_eq!(codes.len(), error_code_all().len());
588    }
589
590    #[test]
591    fn test_lookup_incident_only_code() {
592        // RCH-I012 (ProofRefusal) is an incident-only policy code with no
593        // error-catalog analogue — it must still resolve via `rch error explain`.
594        let e = lookup("RCH-I012").expect("RCH-I012 known");
595        assert_eq!(e.code, "RCH-I012");
596        assert_eq!(e.namespace, CodeNamespace::Incident);
597        assert_eq!(e.category, "proof");
598        assert!(!e.description.is_empty());
599        assert!(!e.remediation.is_empty());
600        assert!(e.requires_restart.is_none());
601    }
602
603    #[test]
604    fn test_lookup_incident_code_case_and_whitespace_tolerant() {
605        let e = lookup("  rch-i001  ").expect("RCH-I001 known");
606        assert_eq!(e.code, "RCH-I001");
607        assert_eq!(e.namespace, CodeNamespace::Incident);
608    }
609
610    #[test]
611    fn test_every_incident_code_resolves_with_description_and_remediation() {
612        for v in IncidentReasonCode::ALL {
613            let e = lookup(v.code()).unwrap_or_else(|| panic!("{} must resolve", v.code()));
614            assert_eq!(e.namespace, CodeNamespace::Incident);
615            assert!(
616                !e.description.is_empty(),
617                "{} has empty description",
618                v.code()
619            );
620            assert!(
621                !e.remediation.is_empty(),
622                "{} has empty remediation",
623                v.code()
624            );
625            assert!(!e.category.is_empty(), "{} has empty category", v.code());
626        }
627    }
628
629    #[test]
630    fn test_list_all_includes_incident_namespace() {
631        let all = list_all();
632        let incident_count = all
633            .iter()
634            .filter(|e| e.namespace == CodeNamespace::Incident)
635            .count();
636        assert_eq!(
637            incident_count,
638            IncidentReasonCode::ALL.len(),
639            "list_all must include every incident reason code"
640        );
641        assert!(incident_count >= 17, "expected at least 17 incident codes");
642    }
643
644    #[test]
645    fn test_render_human_labels_incident_namespace() {
646        let e = lookup("RCH-I011").unwrap();
647        let rendered = render_human(&e);
648        assert!(rendered.contains("RCH-I011"));
649        assert!(rendered.contains("incident (RCH-Innn)"));
650        assert!(rendered.contains("Remediation:"));
651    }
652}