biors 0.47.12

Command-line tools for bio-rs biological AI model input workflows.
use serde_json::Value;
use std::process::{Command, Stdio};

mod common;
use common::ChildInputExt;

#[test]
fn human_errors_include_stable_error_code() {
    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("tokenize")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors tokenize")
        .tap_stdin("ACDE\n");

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stdout.is_empty());

    let stderr = String::from_utf8(output.stderr).expect("stderr is UTF-8");
    assert!(stderr.contains("error[fasta.missing_header]:"));
    assert!(stderr.contains("FASTA input must start with a header line"));
}

#[test]
fn help_snapshot_lists_commands_and_global_json_flag() {
    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--help")
        .output()
        .expect("run biors help");

    assert!(output.status.success());
    assert!(output.stderr.is_empty());

    let stdout = String::from_utf8(output.stdout).expect("help is UTF-8");
    for expected in [
        "Usage: biors [OPTIONS] <COMMAND>",
        "--json",
        "debug",
        "diff",
        "doctor",
        "completions",
        "fasta",
        "inspect",
        "model-input",
        "package",
        "pipeline",
        "seq",
        "tokenizer",
        "tokenize",
    ] {
        assert!(stdout.contains(expected), "help output missing {expected}");
    }
}

#[test]
fn cli_contract_padding_values_match_help() {
    let contract = std::fs::read_to_string(common::repo_root().join("docs/cli-contract.md"))
        .expect("read CLI contract");

    for command in ["model-input", "workflow", "pipeline"] {
        let expected = "[--padding fixed-length|no-padding]";
        assert!(
            contract.contains(expected),
            "CLI contract missing padding values for {command}: {expected}"
        );

        let output = Command::new(env!("CARGO_BIN_EXE_biors"))
            .arg(command)
            .arg("--help")
            .output()
            .expect("run command help");
        assert!(output.status.success());
        assert!(output.stderr.is_empty());

        let help = String::from_utf8(output.stdout).expect("help is UTF-8");
        assert!(
            help.contains("[possible values: fixed-length, no-padding]"),
            "{command} help does not expose documented padding values"
        );
    }
}

#[test]
fn cli_contract_package_options_match_help() {
    let contract = std::fs::read_to_string(common::repo_root().join("docs/cli-contract.md"))
        .expect("read CLI contract");

    let expected_by_command = [
        ("init", ["--doi", "--force"].as_slice()),
        ("convert-project", ["--doi", "--force"].as_slice()),
        (
            "convert",
            [
                "--doi",
                "--license-file",
                "--citation-file",
                "--models-dir",
                "--tokenizers-dir",
                "--vocabs-dir",
                "--pipelines-dir",
                "--fixtures-dir",
                "--observed-dir",
                "--docs-dir",
            ]
            .as_slice(),
        ),
    ];

    for (command, expected_options) in expected_by_command {
        let output = Command::new(env!("CARGO_BIN_EXE_biors"))
            .args(["package", command, "--help"])
            .output()
            .expect("run package command help");
        assert!(output.status.success());
        assert!(output.stderr.is_empty());

        let help = String::from_utf8(output.stdout).expect("help is UTF-8");
        for expected in expected_options {
            assert!(
                help.contains(expected),
                "package {command} help missing expected option {expected}"
            );
            assert!(
                contract.contains(expected),
                "CLI contract missing package {command} option {expected}"
            );
        }
    }
}

#[test]
fn completions_command_outputs_shell_script() {
    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("completions")
        .arg("bash")
        .output()
        .expect("run biors completions");

    assert!(output.status.success());
    assert!(output.stderr.is_empty());

    let stdout = String::from_utf8(output.stdout).expect("completion output is UTF-8");
    assert!(stdout.contains("_biors"));
    assert!(stdout.contains("COMPREPLY"));
}

#[test]
fn malformed_json_input_fails_without_panic() {
    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("validate")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors package validate")
        .tap_stdin("{not valid json");

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "json.invalid");
}

#[test]
fn invalid_utf8_fasta_fails_without_panic() {
    let mut child = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("tokenize")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors tokenize");

    use std::io::Write;
    child
        .stdin
        .as_mut()
        .expect("stdin pipe")
        .write_all(b">seq1\nAC\xffDE\n")
        .expect("write stdin");

    let output = child.wait_with_output().expect("wait for biors");
    assert_eq!(output.status.code(), Some(1));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "io.read_failed");
    assert!(value["error"]["message"]
        .as_str()
        .expect("message")
        .contains("invalid UTF-8"));
}

#[test]
fn manifest_path_traversal_fails_without_panic() {
    let output = Command::new(env!("CARGO_BIN_EXE_biors"))
        .arg("--json")
        .arg("package")
        .arg("validate")
        .arg("-")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn biors package validate")
        .tap_stdin(
            r#"{
              "schema_version": "biors.package.v0",
              "name": "bad-path",
              "model": { "format": "onnx", "path": "../escape.onnx" },
              "preprocessing": [],
              "postprocessing": [],
              "runtime": {
                "backend": "onnx-webgpu",
                "target": "browser-wasm-webgpu"
              },
              "fixtures": []
            }"#,
        );

    assert_eq!(output.status.code(), Some(2));
    assert!(output.stderr.is_empty());

    let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
    assert_eq!(value["error"]["code"], "package.invalid_asset_path");
}