smix-cli 0.1.0

smix — AI-native iOS Simulator automation CLI (cement). v3.1 c12 MVP: doctor + sim subcommands. record/run/repl/watch land in c13/c-final.
//! v5.1 c7 — `scripts/v5/selftest-gate-multi.sh` lint。
//!
//! 钉:文件存在 + 可执行 + bash 语法干净 + 关键子串(capsule up/down +
//! `smix selftest multi` + jq summary.json + N 路 trap)+ exit code 11/12/13/14
//! 字面出现(避免后续 N 化 refactor 时悄悄漏掉)。

use std::path::{Path, PathBuf};
use std::process::Command;

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("smix-cli lives under <repo>/crates")
        .to_path_buf()
}

fn script_path() -> PathBuf {
    repo_root().join("scripts/v5/selftest-gate-multi.sh")
}

#[test]
fn case01_script_exists() {
    let p = script_path();
    assert!(p.is_file(), "missing {p:?} — c7 hasn't landed");
}

#[test]
fn case02_script_executable_or_runnable() {
    // macOS 上 git tracks executable bit;我们最低验"chmod +x 已设"或
    // "bash <path>" 跑得起来(后者就够 — CI 上 bash 直接 invoke)。
    let p = script_path();
    let out = Command::new("bash")
        .args(["-n", p.to_str().unwrap()])
        .output()
        .expect("bash -n");
    assert!(
        out.status.success(),
        "bash -n failed: stderr=\n{}",
        String::from_utf8_lossy(&out.stderr)
    );
}

#[test]
fn case03_mentions_required_subjects() {
    let body = std::fs::read_to_string(script_path()).expect("read script");
    for needle in [
        "smix capsule up",
        "smix capsule down",
        "smix selftest multi",
        "summary.json",
        ".smix/selftest-multi",
        "trap cleanup EXIT",
        "SIMX_UDIDS",
        "SIMX_RUNNER_PORT",
    ] {
        assert!(body.contains(needle), "script body must mention `{needle}`");
    }
}

#[test]
fn case04_mentions_required_exit_codes() {
    let body = std::fs::read_to_string(script_path()).expect("read script");
    for code in [
        "exit 9", "exit 10", "exit 11", "exit 12", "exit 13", "exit 14",
    ] {
        assert!(body.contains(code), "script body must mention `{code}`");
    }
}

#[test]
fn case05_no_simctl_shutdown_all() {
    // 守 [[smix-sim-guard-hook]] — 不要 simctl shutdown all 全局动词。
    let body = std::fs::read_to_string(script_path()).expect("read script");
    assert!(
        !body.contains("simctl shutdown all"),
        "script must not invoke `simctl shutdown all`(违反 smix-sim-guard-hook);用 per-UDID `smix capsule down` 兜底"
    );
    // 守:不要 simctl shutdown 后跟 booted 字面占位符。
    assert!(
        !body.contains("simctl shutdown booted"),
        "script must not invoke `simctl shutdown booted`"
    );
}