rmux 0.10.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn temp_dir(label: &str) -> PathBuf {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock before epoch")
        .as_nanos();
    let path = std::env::temp_dir().join(format!(
        "rmux-release-adversarial-{label}-{}-{nonce}",
        std::process::id()
    ));
    fs::create_dir_all(&path).expect("create fixture directory");
    path
}

fn run(program: &Path, args: &[&str]) -> Output {
    Command::new(program)
        .args(args)
        .current_dir(repo_root())
        .output()
        .unwrap_or_else(|error| panic!("failed to run {}: {error}", program.display()))
}

fn git(root: &Path, args: &[&str]) -> Output {
    Command::new("git")
        .args(args)
        .current_dir(root)
        .output()
        .expect("run git fixture command")
}

fn commit(root: &Path, message: &str) -> String {
    let staged = git(root, &["add", "."]);
    assert!(staged.status.success());
    let committed = git(root, &["commit", "-q", "-m", message]);
    assert!(committed.status.success());
    let sha = git(root, &["rev-parse", "HEAD"]);
    assert!(sha.status.success());
    String::from_utf8(sha.stdout)
        .expect("UTF-8 fixture SHA")
        .trim()
        .to_owned()
}

fn policy_root(root: &Path, sha: &str) -> serde_json::Value {
    let output = run(
        &repo_root().join("scripts/release/policy-root.py"),
        &[
            "--repository-root",
            root.to_str().expect("fixture root"),
            "--source-sha",
            sha,
        ],
    );
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).expect("parse policy root")
}

#[test]
fn policy_root_binds_the_committed_contract_and_git_mode() {
    let root = temp_dir("policy-mode");
    fs::create_dir_all(root.join(".github/release")).expect("create contract directory");
    fs::write(root.join("a.txt"), "same bytes\n").expect("write policy file");
    fs::write(
        root.join(".github/release/candidate-contract.json"),
        r#"{"policy_paths":[".github/release/candidate-contract.json","a.txt"]}"#,
    )
    .expect("write policy contract");
    assert!(git(&root, &["init", "-q"]).status.success());
    assert!(git(&root, &["config", "user.name", "RMUX test"])
        .status
        .success());
    assert!(git(
        &root,
        &["config", "user.email", "rmux-test@example.invalid"]
    )
    .status
    .success());
    let first_sha = commit(&root, "regular mode");
    let first = policy_root(&root, &first_sha);

    let mut permissions = fs::metadata(root.join("a.txt"))
        .expect("policy metadata")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(root.join("a.txt"), permissions).expect("make policy executable");
    let second_sha = commit(&root, "executable mode");
    let second = policy_root(&root, &second_sha);

    assert_eq!(first["algorithm"], "sha256-length-delimited-v2");
    assert_eq!(first["records"][1]["mode"], "100644");
    assert_eq!(first["records"][1]["type"], "blob");
    assert_eq!(second["records"][1]["mode"], "100755");
    assert_ne!(
        first["release_policy_sha256"],
        second["release_policy_sha256"]
    );
    fs::remove_dir_all(root).expect("remove policy fixture");
}

#[test]
fn exact_run_verifier_rejects_backdating_outside_fixtures() {
    let verifier = repo_root().join("scripts/release/verify-fast-run.py");
    let source = fs::read_to_string(&verifier).expect("read verifier");
    assert!(source.contains("load_committed_contract"));
    assert!(!source.contains("parser.add_argument(\"--contract\""));
    let output = run(
        &verifier,
        &[
            "--repository",
            "Helvesec/rmux",
            "--run-id",
            "1",
            "--expected-source-sha",
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
            "--kind",
            "fast",
            "--now",
            "2020-01-01T00:00:00Z",
        ],
    );
    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr)
        .contains("--now is restricted to complete offline test fixtures"));
}

fn timing_run(attempt: u64) -> serde_json::Value {
    serde_json::json!({
        "id": 77,
        "workflow_id": 277622540,
        "path": ".github/workflows/ci.yml",
        "name": "CI",
        "event": "push",
        "head_branch": "main",
        "head_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
        "run_attempt": attempt,
        "status": "completed",
        "conclusion": "success",
        "run_started_at": "2026-07-19T09:00:00Z",
        "updated_at": format!("2026-07-19T09:0{attempt}:00Z")
    })
}

fn timing_args<'a>(run_path: &'a Path, jobs_path: &'a Path) -> Vec<&'a str> {
    vec![
        "--repository",
        "Helvesec/rmux",
        "--run-id",
        "77",
        "--attempt",
        "1",
        "--run-json",
        run_path.to_str().expect("run fixture path"),
        "--jobs-json",
        jobs_path.to_str().expect("jobs fixture path"),
    ]
}

#[test]
fn timing_collector_rejects_fractional_inversion_and_rerun_race() {
    let root = temp_dir("timing-race");
    let run_path = root.join("run.json");
    let run_after_path = root.join("run-after.json");
    let jobs_path = root.join("jobs.json");
    fs::write(&run_path, timing_run(1).to_string()).expect("write run fixture");
    fs::write(
        &jobs_path,
        serde_json::json!({"total_count": 1, "jobs": [{
            "id": 1,
            "run_id": 77,
            "run_attempt": 1,
            "head_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
            "name": "Fractional inversion",
            "status": "completed",
            "conclusion": "success",
            "created_at": "2026-07-19T09:00:00.900Z",
            "started_at": "2026-07-19T09:00:00.100Z",
            "completed_at": "2026-07-19T09:00:01.000Z",
            "labels": ["ubuntu-latest"],
            "steps": []
        }]})
        .to_string(),
    )
    .expect("write jobs fixture");
    let collector = repo_root().join("scripts/release/measure-actions-run.py");
    let inverted = run(&collector, &timing_args(&run_path, &jobs_path));
    assert!(!inverted.status.success());
    assert!(String::from_utf8_lossy(&inverted.stderr).contains("timestamps are not ordered"));

    let mut incomplete: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(&jobs_path).expect("read jobs fixture"))
            .expect("parse jobs fixture");
    incomplete["jobs"][0]["status"] = serde_json::json!("in_progress");
    incomplete["jobs"][0]["completed_at"] = serde_json::Value::Null;
    fs::write(&jobs_path, incomplete.to_string()).expect("write incomplete job");
    let unfinished = run(&collector, &timing_args(&run_path, &jobs_path));
    assert!(!unfinished.status.success());
    assert!(String::from_utf8_lossy(&unfinished.stderr).contains("is not completed"));

    fs::write(&run_after_path, timing_run(2).to_string()).expect("write changed run");
    let mut race_args = timing_args(&run_path, &jobs_path);
    race_args.extend([
        "--run-after-json",
        run_after_path.to_str().expect("run-after fixture path"),
    ]);
    let raced = run(&collector, &race_args);
    assert!(!raced.status.success());
    assert!(
        String::from_utf8_lossy(&raced.stderr).contains("run changed while jobs were paginated")
    );
    fs::remove_dir_all(root).expect("remove timing fixture");
}