Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/src/guardian.rs

//! **THE ESTATE GUARDIAN** (5.4 resilience) — every decision the guardian
//! screens take, compiled for the host and tested there; `pages::guardian`
//! only lays them out.
//!
//! One estate (a Private Gunnar: main + twin) has repair CASES:
//! `detected → investigating → approved → running → done | failed`. A human
//! presses one of four buttons — recharge from twin, review twin, new twin,
//! new server — with a fresh admin passkey; or the estate's AUTO-FIX opt-in
//! (off by default) lets the guardian approve its own case. A button never
//! reaches into the estate: monetize queues a SIGNED order the box pulls, or
//! creates a box through its cloud and the installer lays it.
//!
//! ```text
//! Guardian:     estate | product | main | twin | auto-fix | open case
//! One estate:   boxes (role | last heard | address), the four buttons,
//!               the auto-fix switch, cases (opened | finding | state | remedy | approved by | outcome),
//!               and the newest case's steps and log
//! ```
//!
//! [`estate_text`] draws the screen as plain text — what the tests (and
//! `ui-case.md`) read.

use serde::Deserialize;

use crate::accounts::text_table;
use crate::fmt::{percent_encode, since, Tone};

/// The four buttons: (remedy, label), in screen order.
pub const BUTTONS: [(&str, &str); 4] = [
    ("recharge_from_twin", "Recharge from twin"),
    ("review_twin", "Review twin"),
    ("new_twin", "New twin"),
    ("new_server", "New server"),
];
/// The Guardian list's columns.
pub const ESTATE_COLUMNS: [&str; 6] =
    ["estate", "product", "main", "twin", "auto-fix", "open case"];
/// One estate's box columns.
pub const BOX_COLUMNS: [&str; 3] = ["role", "last heard", "address"];
/// One estate's case columns.
pub const CASE_COLUMNS: [&str; 6] = [
    "opened",
    "finding",
    "state",
    "remedy",
    "approved by",
    "outcome",
];
/// One case's step columns.
pub const STEP_COLUMNS: [&str; 3] = ["step", "state", "detail"];

/// A cell with nothing to say.
pub const NONE: &str = "—";
/// The principal the guardian records when auto-fix approved the case.
pub const AUTO_WORDS: &str = "guardian (auto-fix)";

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct BoxJson {
    pub role: String,
    #[serde(default)]
    pub heard_unix_ms: u64,
    #[serde(default)]
    pub silent: bool,
    #[serde(default)]
    pub laid_ipv4: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct StepJson {
    #[serde(default)]
    pub seq: u64,
    pub words: String,
    pub state: String,
    #[serde(default)]
    pub detail: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct LineJson {
    pub at_unix_ms: u64,
    pub words: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct CaseJson {
    pub id: String,
    pub state: String,
    #[serde(default)]
    pub opened_unix_ms: u64,
    #[serde(default)]
    pub updated_unix_ms: u64,
    #[serde(default)]
    pub finding_code: String,
    #[serde(default)]
    pub finding_words: String,
    #[serde(default)]
    pub evidence: Vec<String>,
    #[serde(default)]
    pub buttons: Vec<String>,
    #[serde(default)]
    pub remedy: String,
    #[serde(default)]
    pub approved_by: String,
    #[serde(default)]
    pub auto: bool,
    #[serde(default)]
    pub steps: Vec<StepJson>,
    #[serde(default)]
    pub log: Vec<LineJson>,
    #[serde(default)]
    pub outcome: String,
    #[serde(default)]
    pub receipt_json: String,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct EstateJson {
    pub instance: String,
    #[serde(default)]
    pub product: String,
    #[serde(default)]
    pub auto_fix: bool,
    #[serde(default)]
    pub boxes: Vec<BoxJson>,
    /// Newest first.
    #[serde(default)]
    pub cases: Vec<CaseJson>,
    /// Its estate bearer (ST-13): until when it opens (0: none, or revoked).
    #[serde(default)]
    pub bearer_until_unix_ms: u64,
    /// When the operator revoked it (0: never).
    #[serde(default)]
    pub bearer_revoked_unix_ms: u64,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct GuardianJson {
    #[serde(default)]
    pub estates: Vec<EstateJson>,
    /// PROD-4: the box did not answer within its bound; this is its state this old.
    #[serde(default)]
    pub stale_secs: Option<u64>,
}

/// One button as the screen draws it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ButtonState {
    pub remedy: &'static str,
    pub label: &'static str,
    pub enabled: bool,
    /// The open case recommends it.
    pub recommended: bool,
    /// Why it is disabled; empty when enabled.
    pub why: String,
}

/// `/guardian/estate?instance=…`
pub fn estate_href(instance: &str) -> String {
    format!("/guardian/estate?instance={}", percent_encode(instance))
}

/// The BFF's path for one estate.
pub fn estate_api(instance: &str) -> String {
    format!("/api/guardian/{}", percent_encode(instance))
}

/// The BFF's path for a press on one estate.
pub fn press_api(instance: &str) -> String {
    format!("{}/press", estate_api(instance))
}

/// The BFF's path for the estate's auto-fix switch.
pub fn auto_fix_api(instance: &str) -> String {
    format!("{}/auto-fix", estate_api(instance))
}

fn in_hand(state: &str) -> bool {
    matches!(state, "detected" | "investigating" | "approved" | "running")
}

fn busy(state: &str) -> bool {
    matches!(state, "approved" | "running")
}

/// The case still in hand (detected, investigating, approved or running), if any.
pub fn open_case(e: &EstateJson) -> Option<&CaseJson> {
    e.cases.iter().find(|c| in_hand(&c.state))
}

/// A remedy's label.
pub fn remedy_label(remedy: &str) -> String {
    BUTTONS
        .iter()
        .find(|(r, _)| *r == remedy)
        .map(|(_, l)| (*l).to_owned())
        .unwrap_or_else(|| remedy.to_owned())
}

/// What colour a case state carries.
pub fn state_tone(state: &str) -> Tone {
    match state {
        "done" => Tone::Pass,
        "failed" => Tone::Bad,
        "approved" | "running" => Tone::Live,
        "detected" | "investigating" => Tone::Warn,
        _ => Tone::Plain,
    }
}

/// **The four buttons.** Pressable unless a case is already approved or
/// running (one repair at a time); the open case's recommendations are marked.
pub fn buttons(e: &EstateJson) -> Vec<ButtonState> {
    let open = open_case(e);
    let why = open
        .filter(|c| busy(&c.state))
        .map(|c| format!("case {} is {}: one repair at a time", c.id, c.state))
        .unwrap_or_default();
    BUTTONS
        .iter()
        .map(|(remedy, label)| ButtonState {
            remedy,
            label,
            enabled: why.is_empty(),
            recommended: open.is_some_and(|c| c.buttons.iter().any(|b| b == remedy)),
            why: why.clone(),
        })
        .collect()
}

/// The auto-fix switch's line.
pub fn auto_fix_line(e: &EstateJson) -> String {
    if e.auto_fix {
        "Auto-fix: ON — the guardian approves its own cases".into()
    } else {
        "Auto-fix: OFF (opt-in) — a person approves every repair".into()
    }
}

/// **The estate bearer's line** (ST-13): live until a date, expired, revoked, or none — and
/// how it is renewed or taken away.
pub fn bearer_line(e: &EstateJson, now_ms: u64) -> String {
    let rotate = "a new enrolment token replaces it";
    match (e.bearer_until_unix_ms, e.bearer_revoked_unix_ms) {
        (_, r) if r > 0 => format!("Estate bearer: REVOKED {} — {rotate}", crate::fmt::date(r)),
        (0, _) => {
            format!("Estate bearer: none — the estate has not enrolled here with one; {rotate}")
        }
        (u, _) if u <= now_ms => {
            format!("Estate bearer: EXPIRED {} — {rotate}", crate::fmt::date(u))
        }
        (u, _) if u == u64::MAX => format!(
            "Estate bearer: live, no expiry — revoke it at the door (attach-revoke); {rotate}"
        ),
        (u, _) => format!(
            "Estate bearer: live until {} — revoke it at the door (attach-revoke); {rotate}",
            crate::fmt::date(u)
        ),
    }
}

fn heard(b: &BoxJson, now_ms: u64) -> String {
    if b.heard_unix_ms == 0 {
        return "never".into();
    }
    let ago = since(b.heard_unix_ms, now_ms);
    if b.silent {
        format!("{ago} · SILENT")
    } else {
        ago
    }
}

fn or_none(s: &str) -> String {
    if s.is_empty() {
        NONE.into()
    } else {
        s.to_owned()
    }
}

/// One box: role | last heard | address.
pub fn box_cells(b: &BoxJson, now_ms: u64) -> [String; 3] {
    [b.role.clone(), heard(b, now_ms), or_none(&b.laid_ipv4)]
}

/// One case: opened | finding | state | remedy | approved by | outcome.
pub fn case_cells(c: &CaseJson, now_ms: u64) -> [String; 6] {
    let by = if c.auto {
        AUTO_WORDS.to_owned()
    } else {
        or_none(&c.approved_by)
    };
    let finding = if c.finding_words.is_empty() {
        c.finding_code.clone()
    } else {
        c.finding_words.clone()
    };
    let remedy = if c.remedy.is_empty() {
        NONE.into()
    } else {
        remedy_label(&c.remedy)
    };
    [
        since(c.opened_unix_ms, now_ms),
        finding,
        c.state.clone(),
        remedy,
        by,
        or_none(&c.outcome),
    ]
}

/// One step: step | state | detail.
pub fn step_cells(s: &StepJson) -> [String; 3] {
    let words = if s.seq > 0 {
        format!("{} (order #{})", s.words, s.seq)
    } else {
        s.words.clone()
    };
    [words, s.state.clone(), or_none(&s.detail)]
}

/// One estate on the Guardian list.
pub fn estate_cells(e: &EstateJson, now_ms: u64) -> [String; 6] {
    let role = |r: &str| {
        e.boxes
            .iter()
            .find(|b| b.role == r)
            .map(|b| heard(b, now_ms))
            .unwrap_or_else(|| NONE.into())
    };
    let open = open_case(e)
        .map(|c| {
            format!(
                "{}: {}",
                c.state,
                if c.finding_words.is_empty() {
                    &c.finding_code
                } else {
                    &c.finding_words
                }
            )
        })
        .unwrap_or_else(|| NONE.into());
    [
        e.instance.clone(),
        e.product.clone(),
        role("main"),
        role("twin"),
        if e.auto_fix {
            "on".into()
        } else {
            "off".into()
        },
        open,
    ]
}

/// The buttons as one text line: `[ Label ]` pressable, `[*Label*]`
/// recommended, `( Label )` disabled.
pub fn buttons_line(e: &EstateJson) -> String {
    buttons(e)
        .iter()
        .map(|b| match (b.enabled, b.recommended) {
            (false, _) => format!("( {} )", b.label),
            (true, true) => format!("[*{}*]", b.label),
            (true, false) => format!("[ {} ]", b.label),
        })
        .collect::<Vec<_>>()
        .join(" ")
}

/// **The Guardian list as text.**
pub fn guardian_text(j: &GuardianJson, now_ms: u64) -> String {
    let rows: Vec<Vec<String>> = j
        .estates
        .iter()
        .map(|e| estate_cells(e, now_ms).to_vec())
        .collect();
    format!("Guardian\n{}", text_table(&ESTATE_COLUMNS, &rows))
}

/// **One estate's case screen as text** — boxes, buttons, auto-fix, cases,
/// and the newest case's steps and log.
pub fn estate_text(e: &EstateJson, now_ms: u64) -> String {
    let mut t = format!("Estate {} ({})\n\n", e.instance, e.product);
    let boxes: Vec<Vec<String>> = e
        .boxes
        .iter()
        .map(|b| box_cells(b, now_ms).to_vec())
        .collect();
    t.push_str(&text_table(&BOX_COLUMNS, &boxes));
    t.push('\n');
    t.push_str(&buttons_line(e));
    t.push('\n');
    if let Some(why) = buttons(e)
        .first()
        .map(|b| b.why.clone())
        .filter(|w| !w.is_empty())
    {
        t.push_str(&format!("  {why}\n"));
    }
    t.push_str(&format!("{}\n", auto_fix_line(e)));
    t.push_str(&format!("{}\n\n", bearer_line(e, now_ms)));
    let cases: Vec<Vec<String>> = e
        .cases
        .iter()
        .map(|c| case_cells(c, now_ms).to_vec())
        .collect();
    t.push_str(&text_table(&CASE_COLUMNS, &cases));
    if let Some(c) = e.cases.first() {
        let mut head = format!("\nCase {} · {}", c.id, c.state);
        if !c.remedy.is_empty() {
            head.push_str(&format!(" · {}", remedy_label(&c.remedy)));
        }
        if c.auto {
            head.push_str(&format!(" · approved by {AUTO_WORDS}"));
        } else if !c.approved_by.is_empty() {
            head.push_str(&format!(" · approved by {}", c.approved_by));
        }
        t.push_str(&head);
        t.push('\n');
        if !c.steps.is_empty() {
            let steps: Vec<Vec<String>> = c.steps.iter().map(|s| step_cells(s).to_vec()).collect();
            t.push_str(&text_table(&STEP_COLUMNS, &steps));
        }
        if !c.evidence.is_empty() {
            t.push_str("Evidence\n");
            for ev in &c.evidence {
                t.push_str(&format!("  · {ev}\n"));
            }
        }
        if !c.log.is_empty() {
            t.push_str("Log\n");
            for l in &c.log {
                t.push_str(&format!("  {} {}\n", since(l.at_unix_ms, now_ms), l.words));
            }
        }
        if !c.receipt_json.is_empty() {
            t.push_str(&format!("Receipt {}\n", c.receipt_json));
        }
    }
    t
}

#[cfg(test)]
mod tests {
    use super::*;

    const NOW: u64 = 1_760_090_000_000;

    fn twin_lost(state: &str) -> EstateJson {
        EstateJson {
            instance: "acme".into(),
            product: "gunnar".into(),
            auto_fix: false,
            boxes: vec![
                BoxJson {
                    role: "main".into(),
                    heard_unix_ms: NOW - 20_000,
                    silent: false,
                    laid_ipv4: "94.237.1.10".into(),
                },
                BoxJson {
                    role: "twin".into(),
                    heard_unix_ms: NOW - 6 * 60_000,
                    silent: true,
                    laid_ipv4: "94.237.1.11".into(),
                },
            ],
            cases: vec![
                CaseJson {
                    id: "acme-2".into(),
                    state: state.into(),
                    opened_unix_ms: NOW - 5 * 60_000,
                    finding_code: "twin_silent".into(),
                    finding_words: "the twin has not called home for 6 min".into(),
                    evidence: vec!["twin last heard 6 min ago".into()],
                    buttons: vec!["new_twin".into(), "review_twin".into()],
                    remedy: if state == "detected" || state == "investigating" {
                        String::new()
                    } else {
                        "new_twin".into()
                    },
                    approved_by: if state == "detected" || state == "investigating" {
                        String::new()
                    } else {
                        "rickard".into()
                    },
                    steps: vec![
                        StepJson {
                            seq: 0,
                            words: "fence the old twin (cloud: destroy server)".into(),
                            state: "met".into(),
                            detail: String::new(),
                        },
                        StepJson {
                            seq: 0,
                            words: "lay a new twin (installer)".into(),
                            state: "running".into(),
                            detail: "relay 94.237.1.12".into(),
                        },
                        StepJson {
                            seq: 12,
                            words: "main pulls adopt_twin".into(),
                            state: "pending".into(),
                            detail: String::new(),
                        },
                    ],
                    log: vec![LineJson {
                        at_unix_ms: NOW - 5 * 60_000,
                        words: "detected: twin silent".into(),
                    }],
                    ..Default::default()
                },
                CaseJson {
                    id: "acme-1".into(),
                    state: "done".into(),
                    opened_unix_ms: NOW - 3 * 86_400_000,
                    finding_code: "main_data_lost".into(),
                    finding_words: "main lost its data set".into(),
                    remedy: "recharge_from_twin".into(),
                    approved_by: "rickard".into(),
                    outcome: "recharged 1.2 GiB from the twin".into(),
                    ..Default::default()
                },
            ],
            ..Default::default()
        }
    }

    /// ★ **No open case: all four buttons are pressable** — a human may start
    /// any repair; none is recommended.
    #[test]
    fn with_no_case_open_all_four_buttons_are_pressable() {
        let mut e = twin_lost("done");
        e.cases.remove(0);
        let b = buttons(&e);
        assert_eq!(
            b.iter().map(|b| b.label).collect::<Vec<_>>(),
            [
                "Recharge from twin",
                "Review twin",
                "New twin",
                "New server"
            ]
        );
        assert!(
            b.iter()
                .all(|b| b.enabled && !b.recommended && b.why.is_empty()),
            "{b:?}"
        );
        assert!(open_case(&e).is_none());
    }

    /// ★ **A detected case recommends its remedies; every button stays pressable.**
    #[test]
    fn a_detected_case_marks_what_it_recommends() {
        let e = twin_lost("detected");
        assert_eq!(open_case(&e).map(|c| c.id.as_str()), Some("acme-2"));
        let b = buttons(&e);
        assert!(b.iter().all(|b| b.enabled), "{b:?}");
        assert_eq!(
            b.iter()
                .filter(|b| b.recommended)
                .map(|b| b.remedy)
                .collect::<Vec<_>>(),
            ["review_twin", "new_twin"]
        );
        assert_eq!(
            buttons_line(&e),
            "[ Recharge from twin ] [*Review twin*] [*New twin*] [ New server ]"
        );
    }

    /// ★ **One repair at a time**: while a case is approved or running, every
    /// button is disabled and says which case holds the estate.
    #[test]
    fn a_running_case_disables_every_button_and_says_why() {
        for state in ["approved", "running"] {
            let e = twin_lost(state);
            let b = buttons(&e);
            assert!(
                b.iter()
                    .all(|b| !b.enabled && b.why.contains("acme-2") && b.why.contains(state)),
                "{state}: {b:?}"
            );
            assert_eq!(
                buttons_line(&e),
                "( Recharge from twin ) ( Review twin ) ( New twin ) ( New server )"
            );
        }
    }

    /// ★ **Auto-fix is an opt-in, OFF by default**, and the line says what
    /// each position means; an auto-approved case names the guardian.
    #[test]
    fn the_estate_bearer_is_listed_live_expired_revoked_or_none() {
        let mut e = twin_lost("detected");
        assert!(
            bearer_line(&e, NOW).starts_with("Estate bearer: none"),
            "{}",
            bearer_line(&e, NOW)
        );
        e.bearer_until_unix_ms = NOW + 86_400_000;
        assert!(bearer_line(&e, NOW).starts_with(&format!(
            "Estate bearer: live until {}",
            crate::fmt::date(NOW + 86_400_000)
        )));
        assert!(bearer_line(&e, NOW + 86_400_000).starts_with("Estate bearer: EXPIRED"));
        e.bearer_revoked_unix_ms = NOW;
        assert!(bearer_line(&e, NOW).starts_with("Estate bearer: REVOKED"));
        assert!(
            estate_text(&e, NOW).contains("Estate bearer: REVOKED"),
            "the case screen lists it"
        );
    }

    #[test]
    fn auto_fix_is_an_opt_in_and_an_auto_case_names_the_guardian() {
        let mut e = twin_lost("running");
        assert_eq!(
            auto_fix_line(&e),
            "Auto-fix: OFF (opt-in) — a person approves every repair"
        );
        e.auto_fix = true;
        assert_eq!(
            auto_fix_line(&e),
            "Auto-fix: ON — the guardian approves its own cases"
        );
        e.cases[0].auto = true;
        assert_eq!(case_cells(&e.cases[0], NOW)[4], AUTO_WORDS);
        assert!(!EstateJson::default().auto_fix, "absent means off");
    }

    /// The cells: a silent box says so; a case reads opened | finding | state |
    /// remedy | approved by | outcome; a step carries its order seq.
    #[test]
    fn boxes_cases_and_steps_read_as_cells() {
        let e = twin_lost("running");
        assert_eq!(
            box_cells(&e.boxes[0], NOW),
            ["main".to_string(), "just now".into(), "94.237.1.10".into()]
        );
        assert_eq!(
            box_cells(&e.boxes[1], NOW),
            [
                "twin".to_string(),
                "6 min ago · SILENT".into(),
                "94.237.1.11".into()
            ]
        );
        assert_eq!(
            box_cells(
                &BoxJson {
                    role: "twin".into(),
                    ..Default::default()
                },
                NOW
            )[1],
            "never"
        );
        assert_eq!(
            case_cells(&e.cases[0], NOW),
            [
                "5 min ago".to_string(),
                "the twin has not called home for 6 min".into(),
                "running".into(),
                "New twin".into(),
                "rickard".into(),
                NONE.into()
            ]
        );
        assert_eq!(
            case_cells(&e.cases[1], NOW)[3..],
            [
                "Recharge from twin".to_string(),
                "rickard".into(),
                "recharged 1.2 GiB from the twin".into()
            ]
        );
        assert_eq!(
            case_cells(&twin_lost("detected").cases[0], NOW)[3..5],
            [NONE.to_string(), NONE.into()]
        );
        assert_eq!(
            step_cells(&e.cases[0].steps[2]),
            [
                "main pulls adopt_twin (order #12)".to_string(),
                "pending".into(),
                NONE.into()
            ]
        );
        assert_eq!(step_cells(&e.cases[0].steps[1])[2], "relay 94.237.1.12");
        assert_eq!(
            (
                state_tone("done"),
                state_tone("failed"),
                state_tone("running"),
                state_tone("detected")
            ),
            (Tone::Pass, Tone::Bad, Tone::Live, Tone::Warn)
        );
    }

    /// The list: one line per estate, the boxes' last heard, auto-fix and the
    /// open case (or none).
    #[test]
    fn the_guardian_list_reads_one_line_per_estate() {
        let e = twin_lost("detected");
        assert_eq!(
            estate_cells(&e, NOW),
            [
                "acme".to_string(),
                "gunnar".into(),
                "just now".into(),
                "6 min ago · SILENT".into(),
                "off".into(),
                "detected: the twin has not called home for 6 min".into()
            ]
        );
        let mut quiet = twin_lost("done");
        quiet.cases.remove(0);
        quiet.auto_fix = true;
        assert_eq!(
            estate_cells(&quiet, NOW)[4..],
            ["on".to_string(), NONE.into()]
        );
        let text = guardian_text(
            &GuardianJson {
                estates: vec![e],
                ..Default::default()
            },
            NOW,
        );
        assert!(
            text.starts_with("Guardian\nestate | product | main"),
            "{text}"
        );
        assert!(text.contains("acme"), "{text}");
    }

    /// ★ **The case screen as text** — what `ui-case.md` shows the owner.
    #[test]
    fn one_estate_reads_as_boxes_buttons_auto_fix_cases_then_the_newest_case() {
        let t = estate_text(&twin_lost("running"), NOW);
        let at = |needle: &str| {
            t.find(needle)
                .unwrap_or_else(|| panic!("{needle:?} missing from:\n{t}"))
        };
        assert!(
            at("Estate acme (gunnar)") < at("role | last heard")
                && at("role | last heard") < at("( Recharge from twin )")
        );
        assert!(
            at("( New server )") < at("Auto-fix: OFF") && at("Auto-fix: OFF") < at("| finding")
        );
        assert!(at("| finding") < at("Case acme-2 · running · New twin · approved by rickard"));
        assert!(
            at("Case acme-2") < at("\nstep ")
                && at("\nstep ") < at("main pulls adopt_twin (order #12)")
        );
        assert!(at("Evidence") < at("twin last heard 6 min ago"));
        assert!(
            t.contains("Log") && t.contains("detected: twin silent"),
            "{t}"
        );
    }

    /// Instances are path segments and query values: encoded.
    #[test]
    fn an_estate_is_addressed_by_its_instance() {
        assert_eq!(
            estate_href("acme ab"),
            "/guardian/estate?instance=acme%20ab"
        );
        assert_eq!(estate_api("acme"), "/api/guardian/acme");
        assert_eq!(press_api("acme"), "/api/guardian/acme/press");
        assert_eq!(auto_fix_api("acme"), "/api/guardian/acme/auto-fix");
        assert_eq!(remedy_label("new_server"), "New server");
        assert_eq!(remedy_label("odd"), "odd");
    }
}