praefectus 0.8.3

Policy-neutral, durable desktop action execution protocol
use std::io::Write;
use std::process::{Command, Stdio};

fn run(arguments: &[&str], stdin: &str) -> std::process::Output {
    run_with_env(arguments, stdin, &[])
}

fn run_with_env(
    arguments: &[&str],
    stdin: &str,
    env: &[(&str, Option<&std::path::Path>)],
) -> std::process::Output {
    let mut command = Command::new(env!("CARGO_BIN_EXE_praefectus"));
    command
        .args(arguments)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    for (key, value) in env {
        match value {
            Some(path) => {
                command.env(key, path);
            }
            None => {
                command.env_remove(key);
            }
        }
    }
    let mut child = command.spawn().expect("spawn CLI");
    if let Some(mut child_stdin) = child.stdin.take() {
        let _ = child_stdin.write_all(stdin.as_bytes());
    }
    child.wait_with_output().expect("CLI output")
}

fn error(output: &std::process::Output) -> serde_json::Value {
    serde_json::from_slice(&output.stdout).expect("JSON error envelope")
}

#[test]
fn usage_errors_are_json_with_exit_two() {
    for arguments in [&[][..], &["unknown"][..], &["status"][..]] {
        let output = run(arguments, "");
        assert_eq!(output.status.code(), Some(2));
        assert_eq!(error(&output)["ok"], false);
        assert_eq!(error(&output)["error"]["code"], "usage");
    }
}

#[test]
fn capabilities_and_status_reject_extra_or_unknown_arguments() {
    for (arguments, message) in [
        (
            &["capabilities", "extra"][..],
            "capabilities does not accept positional arguments",
        ),
        (
            &["capabilities", "--unknown"][..],
            "unknown option: --unknown",
        ),
        (
            &["status", "operation", "extra"][..],
            "status accepts exactly one operation ID",
        ),
        (
            &["status", "operation", "--unknown"][..],
            "unknown option: --unknown",
        ),
        (
            &["status", "operation", "--ledger"][..],
            "--ledger requires a path",
        ),
        (
            &["status", "operation", "--ledger", "one", "--ledger", "two"][..],
            "--ledger may only be specified once",
        ),
        (
            &["surfaces", "extra"][..],
            "surfaces does not accept positional arguments",
        ),
        (
            &["observe-surface"][..],
            "observe-surface requires a surface ID",
        ),
        (
            &["observe-surface", "one", "two"][..],
            "observe-surface accepts exactly one surface ID",
        ),
        (
            &["allow-global-input", "extra"][..],
            "allow-global-input accepts no arguments, allow, deny, or --no-yolo",
        ),
    ] {
        let output = run(arguments, "");
        assert_eq!(output.status.code(), Some(2));
        let value = error(&output);
        assert_eq!(value["ok"], false);
        assert_eq!(value["error"]["code"], "usage");
        assert_eq!(value["error"]["message"], message);
        assert!(output.stderr.is_empty());
    }
}

#[test]
fn success_uses_the_stable_json_envelope() {
    let output = run(&["capabilities"], "");
    assert_eq!(output.status.code(), Some(0));
    let value: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("JSON success envelope");
    assert_eq!(value["ok"], true);
    assert!(value["data"].is_object());
    assert_eq!(value["data"]["session_isolation"], "shared_desktop");
    assert_eq!(value["data"]["permissions"]["global_input"], true);
}

#[test]
fn no_yolo_persists_global_input_restriction() {
    let directory = tempfile::tempdir().expect("temp directory");
    let env = [
        ("XDG_STATE_HOME", Some(directory.path())),
        ("HOME", Some(directory.path())),
        ("LOCALAPPDATA", Some(directory.path())),
        ("PRAEFECTUS_ALLOW_GLOBAL_INPUT", None),
        ("PRAEFECTUS_NO_YOLO", None),
    ];

    let unrestricted = run_with_env(&["allow-global-input"], "", &env);
    assert_eq!(unrestricted.status.code(), Some(0));
    let unrestricted_value: serde_json::Value =
        serde_json::from_slice(&unrestricted.stdout).expect("JSON success envelope");
    assert_eq!(unrestricted_value["ok"], true);
    assert_eq!(unrestricted_value["data"]["allowed"], true);
    assert_eq!(unrestricted_value["data"]["no_yolo"], false);

    let restricted = run_with_env(&["allow-global-input", "--no-yolo"], "", &env);
    assert_eq!(restricted.status.code(), Some(0));
    let restricted_value: serde_json::Value =
        serde_json::from_slice(&restricted.stdout).expect("JSON success envelope");
    assert_eq!(restricted_value["ok"], true);
    assert_eq!(restricted_value["data"]["allowed"], false);
    assert_eq!(restricted_value["data"]["no_yolo"], true);
    assert_eq!(restricted_value["data"]["persisted"], true);

    let restored = run_with_env(&["allow-global-input", "allow"], "", &env);
    assert_eq!(restored.status.code(), Some(0));
    let restored_value: serde_json::Value =
        serde_json::from_slice(&restored.stdout).expect("JSON success envelope");
    assert_eq!(restored_value["ok"], true);
    assert_eq!(restored_value["data"]["allowed"], true);
    assert_eq!(restored_value["data"]["persisted"], false);
}

#[test]
fn execute_requires_a_trusted_library_host() {
    let output = run(&["execute"], "{}");
    assert_eq!(output.status.code(), Some(2));
    assert_eq!(error(&output)["error"]["code"], "usage");
    assert!(
        error(&output)["error"]["message"]
            .as_str()
            .expect("message")
            .contains("host-injected trusted AuthorityVerifier")
    );
    assert!(output.stderr.is_empty());
}

#[test]
fn observe_uses_a_stable_json_envelope() {
    let output = run(&["observe"], "");
    assert!(matches!(output.status.code(), Some(0 | 3)));
    let value = error(&output);
    assert!(value["ok"].is_boolean());
    if output.status.code() == Some(3) {
        assert_eq!(value["error"]["code"], "observation_error");
    }
}

#[test]
fn surface_commands_use_stable_json_envelopes() {
    let output = run(&["surfaces"], "");
    assert!(matches!(output.status.code(), Some(0 | 3)));
    let value = error(&output);
    assert!(value["ok"].is_boolean());
    if output.status.code() == Some(0) {
        assert!(value["data"].is_array());
    } else {
        assert_eq!(value["error"]["code"], "observation_error");
    }

    let output = run(&["observe-surface", "0"], "");
    assert!(matches!(output.status.code(), Some(0 | 3)));
    let value = error(&output);
    assert!(value["ok"].is_boolean());
    if output.status.code() == Some(3) {
        assert_eq!(value["error"]["code"], "observation_error");
    }
}