mabi-cli 1.7.0

Mabinogion - Industrial Protocol Simulator CLI
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

const STALE_PHRASES: &[&str] = &[
    concat!("Industrial Protocol ", "Simulator Server"),
    concat!("OT Protocol ", "Simulator"),
    concat!("trap", "-", "sim"),
    concat!("TRAP protocol ", "simulator"),
];

const CRATE_READMES: &[(&str, &str)] = &[
    ("mabi-core", "crates/mabi-core/README.md"),
    ("mabi-runtime", "crates/mabi-runtime/README.md"),
    ("mabi-cli", "crates/mabi-cli/README.md"),
    ("mabi-modbus", "crates/mabi-modbus/README.md"),
    ("mabi-opcua", "crates/mabi-opcua/README.md"),
    ("mabi-bacnet", "crates/mabi-bacnet/README.md"),
    ("mabi-knx", "crates/mabi-knx/README.md"),
    ("mabi-scenario", "crates/mabi-scenario/README.md"),
    ("mabi-chaos", "crates/mabi-chaos/README.md"),
];

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../..")
        .canonicalize()
        .expect("workspace root should resolve")
}

fn read(root: &Path, relative_path: &str) -> String {
    let path = root.join(relative_path);
    fs::read_to_string(&path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()))
}

fn assert_no_stale_phrases(path: &str, contents: &str) {
    for phrase in STALE_PHRASES {
        assert!(
            !contents.contains(phrase),
            "{path} should not contain stale product phrase {phrase:?}"
        );
    }
}

fn collect_text_files(path: &Path, files: &mut Vec<PathBuf>) {
    for entry in fs::read_dir(path)
        .unwrap_or_else(|error| panic!("failed to read directory {}: {error}", path.display()))
    {
        let entry = entry.expect("directory entry should be readable");
        let path = entry.path();
        if path.is_dir() {
            collect_text_files(&path, files);
            continue;
        }
        let extension = path.extension().and_then(|value| value.to_str());
        if matches!(extension, Some("md" | "rs" | "yaml" | "yml" | "toml")) {
            files.push(path);
        }
    }
}

fn is_runtime_legacy_identifier(path: &Path, contents: &str) -> bool {
    let path = path.to_string_lossy();
    contents.contains(concat!("trap", "-", "simulator"))
        && (path.ends_with("crates/mabi-core/src/config/mod.rs")
            || path.ends_with("crates/mabi-core/src/logging/config.rs")
            || path.ends_with("crates/mabi-core/src/logging/mod.rs")
            || path.ends_with("crates/mabi-opcua/src/factory.rs"))
}

#[test]
fn public_docs_use_product_family_positioning() {
    let root = repo_root();
    let root_readme = read(&root, "README.md");
    let cli_docs = read(&root, "docs/cli/README.md");
    let docs_index = read(&root, "docs/README.md");

    for (path, contents) in [
        ("README.md", root_readme.as_str()),
        ("docs/cli/README.md", cli_docs.as_str()),
        ("docs/README.md", docs_index.as_str()),
    ] {
        assert_no_stale_phrases(path, contents);
        for required in [
            "protocol resilience engine",
            "Mabinogion trials",
            "Forge",
            "Trials",
            "official certification",
        ] {
            assert!(
                contents.contains(required),
                "{path} should contain product-family phrase {required:?}"
            );
        }
    }
}

#[test]
fn crate_readmes_are_crate_specific_and_boundary_aware() {
    let root = repo_root();
    for (crate_name, path) in CRATE_READMES {
        let contents = read(&root, path);
        assert_no_stale_phrases(path, &contents);
        assert!(
            contents.contains(crate_name),
            "{path} should identify {crate_name}"
        );
        for required in [
            "What this crate owns",
            "How it fits in Mabinogion",
            "Versioning / contracts",
            "Not owned here",
            "Mabinogion",
            "official certification",
        ] {
            assert!(
                contents.contains(required),
                "{path} should contain crate README section or boundary {required:?}"
            );
        }
    }
}

#[test]
fn source_and_docs_do_not_reintroduce_stale_product_phrases() {
    let root = repo_root();
    let mut files = vec![root.join("README.md")];
    collect_text_files(&root.join("crates"), &mut files);
    collect_text_files(&root.join("docs"), &mut files);

    for path in files {
        let contents = fs::read_to_string(&path)
            .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
        if is_runtime_legacy_identifier(&path, &contents) {
            continue;
        }
        let relative = path
            .strip_prefix(&root)
            .expect("path should be inside root")
            .display()
            .to_string();
        assert_no_stale_phrases(&relative, &contents);
    }
}

#[test]
fn cli_help_uses_product_family_wording() {
    let output = Command::new(env!("CARGO_BIN_EXE_mabi"))
        .arg("--help")
        .output()
        .expect("mabi --help should run");
    assert!(
        output.status.success(),
        "mabi --help failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let help = String::from_utf8_lossy(&output.stdout);
    assert_no_stale_phrases("mabi --help", &help);
    for required in [
        "protocol resilience engine for Mabinogion trials",
        "shared runtime",
        "Forge",
        "Trials",
        "runner surfaces",
    ] {
        assert!(
            help.contains(required),
            "mabi --help should contain {required:?}"
        );
    }
    assert!(
        !help.contains("Industrial protocol simulator for testing and development"),
        "mabi --help should not use the old about text"
    );
}

#[test]
fn serve_help_keeps_local_execution_without_simulator_only_positioning() {
    let output = Command::new(env!("CARGO_BIN_EXE_mabi"))
        .args(["serve", "--help"])
        .output()
        .expect("mabi serve --help should run");
    assert!(
        output.status.success(),
        "mabi serve --help failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let help = String::from_utf8_lossy(&output.stdout);
    assert_no_stale_phrases("mabi serve --help", &help);
    assert!(help.contains("Run a local protocol service through the shared runtime"));
}