use serde_json::Value;
use std::process::Command;
mod common;
use common::{ChildInputExt, TempDir};
const V0_MANIFEST: &str = r#"{
"schema_version": "biors.package.v0",
"name": "protein-seed",
"model": {
"format": "onnx",
"path": "models/protein-seed.onnx",
"checksum": "sha256:2c1da72b15fab35bd6f1bb62f5037b936e26e6413a220fa9afe5a64bce0df68d"
},
"tokenizer": {
"name": "protein-20",
"path": "tokenizers/protein-20.json",
"contract_version": "protein-20.v0"
},
"vocab": {
"name": "protein-20",
"path": "vocabs/protein-20.json",
"contract_version": "protein-20.v0"
},
"preprocessing": [],
"postprocessing": [],
"runtime": {
"backend": "onnx-webgpu",
"target": "browser-wasm-webgpu"
},
"fixtures": [
{
"name": "tiny-protein",
"input": "fixtures/tiny.fasta",
"expected_output": "fixtures/tiny.output.json"
}
]
}"#;
#[test]
fn package_convert_writes_v1_manifest_with_author_metadata() {
let temp = TempDir::new("package-convert");
let input = temp.write("manifest.v0.json", V0_MANIFEST);
let output_path = temp.path().join("manifest.json");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("convert")
.arg(input)
.arg("--output")
.arg(&output_path)
.args(conversion_metadata_args())
.output()
.expect("run biors package convert");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());
let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON output");
assert_eq!(value["data"]["report"]["package"], "protein-seed");
assert_eq!(value["data"]["report"]["from"], "biors.package.v0");
assert_eq!(value["data"]["report"]["to"], "biors.package.v1");
assert_eq!(value["data"]["report"]["converted"], true);
assert_eq!(value["data"]["report"]["metadata_supplied"], true);
assert!(value["data"]["report"]["manifest_sha256"]
.as_str()
.expect("manifest hash")
.starts_with("sha256:"));
let manifest = &value["data"]["manifest"];
assert_eq!(manifest["schema_version"], "biors.package.v1");
assert_eq!(manifest["package_layout"]["manifest"], "manifest.json");
assert_eq!(manifest["package_layout"]["models"], "models");
assert_eq!(manifest["package_layout"]["tokenizers"], "tokenizers");
assert_eq!(manifest["package_layout"]["vocabs"], "vocabs");
assert_eq!(manifest["package_layout"]["fixtures"], "fixtures");
assert_eq!(manifest["package_layout"]["docs"], "docs");
assert_eq!(manifest["metadata"]["license"]["expression"], "CC0-1.0");
assert_eq!(
manifest["metadata"]["citation"]["preferred_citation"],
"bio-rs converted fixture"
);
assert_eq!(
manifest["metadata"]["model_card"]["path"],
"docs/model-card.md"
);
assert_eq!(
manifest["metadata"]["model_card"]["intended_use"][0],
"CLI conversion test"
);
assert_eq!(
manifest["metadata"]["model_card"]["limitations"][0],
"Not for inference"
);
let written: Value = serde_json::from_slice(
&std::fs::read(output_path).expect("read converted manifest from output path"),
)
.expect("written manifest JSON");
assert_eq!(written, *manifest);
}
#[test]
fn package_convert_reports_missing_v1_metadata() {
let output = common::spawn_biors(&[
"--json",
"package",
"convert",
"-",
"--to",
"biors.package.v1",
])
.tap_stdin(V0_MANIFEST);
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.conversion_missing_metadata"
);
}
#[test]
fn package_convert_project_creates_valid_package_skeleton() {
let temp = TempDir::new("package-convert-project");
let project = temp.path().join("python-project");
std::fs::create_dir_all(&project).expect("create python project");
std::fs::write(project.join("model.onnx"), b"placeholder onnx").expect("write model");
std::fs::write(
project.join("tokenizer_config.json"),
r#"{
"tokenizer_class": "BertTokenizer",
"do_lower_case": false,
"cls_token": "[CLS]",
"sep_token": "[SEP]",
"pad_token": "[PAD]",
"unk_token": "[UNK]",
"mask_token": "[MASK]"
}"#,
)
.expect("write tokenizer config");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--output")
.arg(&output_dir)
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());
let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON output");
assert_eq!(value["data"]["package"], "protein-project");
assert!(value["data"]["manifest_sha256"]
.as_str()
.expect("manifest hash")
.starts_with("sha256:"));
let manifest_path = output_dir.join("manifest.json");
let manifest: Value =
serde_json::from_slice(&std::fs::read(&manifest_path).expect("read generated manifest"))
.expect("manifest JSON");
assert_eq!(manifest["schema_version"], "biors.package.v1");
assert_eq!(manifest["tokenizer"]["name"], "protein-20-special");
assert_eq!(
manifest["preprocessing"][0]["config"]["path"],
"pipelines/protein.toml"
);
let pipeline_config =
std::fs::read_to_string(output_dir.join("pipelines/protein.toml")).expect("read pipeline");
assert!(pipeline_config.contains(r#"profile = "protein-20""#));
let validate = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("validate")
.arg(&manifest_path)
.output()
.expect("validate generated package");
assert!(
validate.status.success(),
"stderr: {}",
String::from_utf8_lossy(&validate.stderr)
);
}
#[test]
fn package_convert_project_rejects_ambiguous_model_candidates() {
let temp = TempDir::new("package-convert-project-ambiguous");
let project = temp.path().join("python-project");
std::fs::create_dir_all(project.join("exports")).expect("create exports");
std::fs::write(project.join("exports/a.onnx"), b"a").expect("write model a");
std::fs::write(project.join("exports/b.onnx"), b"b").expect("write model b");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("--json")
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--output")
.arg(temp.path().join("package"))
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
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.project_model_ambiguous");
let candidates = value["error"]["details"]["candidates"]
.as_array()
.expect("candidate list");
assert_eq!(candidates.len(), 2);
assert!(candidates[0]
.as_str()
.expect("candidate")
.ends_with("a.onnx"));
assert!(candidates[1]
.as_str()
.expect("candidate")
.ends_with("b.onnx"));
}
#[test]
fn package_convert_project_skips_generated_model_directories() {
let temp = TempDir::new("package-convert-project-skip-generated");
let project = temp.path().join("python-project");
std::fs::create_dir_all(project.join(".venv/cache")).expect("create cache");
std::fs::create_dir_all(project.join("export")).expect("create export");
std::fs::write(project.join(".venv/cache/cached.onnx"), b"cached").expect("write cached model");
std::fs::write(project.join("export/real.onnx"), b"real").expect("write real model");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--output")
.arg(&output_dir)
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
std::fs::read(output_dir.join("models/real.onnx")).expect("read packaged model"),
b"real"
);
assert!(
!output_dir.join("models/cached.onnx").exists(),
"generated/cache model must not be packaged by default"
);
}
#[test]
fn package_convert_project_skips_generated_tokenizer_config_directories() {
let temp = TempDir::new("package-convert-project-skip-tokenizer-generated");
let project = temp.path().join("python-project");
std::fs::create_dir_all(project.join(".venv/cache")).expect("create cache");
std::fs::create_dir_all(project.join("export")).expect("create export");
let model = project.join("export/real.onnx");
std::fs::write(&model, b"real").expect("write real model");
std::fs::write(
project.join(".venv/cache/tokenizer_config.json"),
r#"{"profile":"protein-20","add_special_tokens":false}"#,
)
.expect("write cached tokenizer config");
std::fs::write(
project.join("export/tokenizer_config.json"),
r#"{"profile":"protein-20-special","add_special_tokens":true}"#,
)
.expect("write exported tokenizer config");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--model")
.arg(&model)
.arg("--output")
.arg(&output_dir)
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let manifest: Value = serde_json::from_slice(
&std::fs::read(output_dir.join("manifest.json")).expect("read generated manifest"),
)
.expect("manifest JSON");
assert_eq!(manifest["tokenizer"]["name"], "protein-20-special");
assert!(output_dir
.join("tokenizers/protein-20-special.json")
.exists());
assert!(!output_dir.join("tokenizers/protein-20.json").exists());
}
#[test]
fn package_convert_project_rejects_ambiguous_tokenizer_config_candidates() {
let temp = TempDir::new("package-convert-project-ambiguous-tokenizer");
let project = temp.path().join("python-project");
std::fs::create_dir_all(project.join("export-a")).expect("create export a");
std::fs::create_dir_all(project.join("export-b")).expect("create export b");
let model = project.join("model.onnx");
std::fs::write(&model, b"model").expect("write model");
std::fs::write(
project.join("export-a/tokenizer_config.json"),
r#"{"profile":"protein-20","add_special_tokens":false}"#,
)
.expect("write tokenizer config a");
std::fs::write(
project.join("export-b/tokenizer_config.json"),
r#"{"profile":"protein-20-special","add_special_tokens":true}"#,
)
.expect("write tokenizer config b");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("--json")
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--model")
.arg(&model)
.arg("--output")
.arg(temp.path().join("package"))
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
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.project_tokenizer_config_ambiguous"
);
let candidates = value["error"]["details"]["candidates"]
.as_array()
.expect("candidate list");
assert_eq!(candidates.len(), 2);
assert!(candidates[0]
.as_str()
.expect("candidate")
.ends_with("export-a/tokenizer_config.json"));
assert!(candidates[1]
.as_str()
.expect("candidate")
.ends_with("export-b/tokenizer_config.json"));
}
#[test]
fn package_convert_project_accepts_explicit_tokenizer_config_override() {
let temp = TempDir::new("package-convert-project-tokenizer-override");
let project = temp.path().join("python-project");
std::fs::create_dir_all(project.join("export-a")).expect("create export a");
std::fs::create_dir_all(project.join("export-b")).expect("create export b");
let model = project.join("model.onnx");
std::fs::write(&model, b"model").expect("write model");
std::fs::write(
project.join("export-a/tokenizer_config.json"),
r#"{"profile":"protein-20","add_special_tokens":false}"#,
)
.expect("write tokenizer config a");
let intended_tokenizer = project.join("export-b/tokenizer_config.json");
std::fs::write(
&intended_tokenizer,
r#"{"profile":"protein-20-special","add_special_tokens":true}"#,
)
.expect("write tokenizer config b");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("convert-project")
.arg(&project)
.arg("--model")
.arg(&model)
.arg("--tokenizer-config")
.arg(&intended_tokenizer)
.arg("--output")
.arg(&output_dir)
.arg("--name")
.arg("protein-project")
.args(skeleton_metadata_args())
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.output()
.expect("run biors package convert-project");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let manifest: Value = serde_json::from_slice(
&std::fs::read(output_dir.join("manifest.json")).expect("read generated manifest"),
)
.expect("manifest JSON");
assert_eq!(manifest["tokenizer"]["name"], "protein-20-special");
assert!(output_dir
.join("tokenizers/protein-20-special.json")
.exists());
}
#[test]
fn package_init_infers_onnx_runtime_defaults_from_extension() {
let manifest = run_package_init_with_model("model.onnx");
assert_eq!(manifest["model"]["format"], "onnx");
assert_eq!(manifest["model"]["path"], "models/model.onnx");
assert_eq!(manifest["runtime"]["backend"], "onnx-webgpu");
assert_eq!(manifest["runtime"]["target"], "browser-wasm-webgpu");
assert_eq!(manifest["runtime"]["version"], "onnx-webgpu.v0");
}
#[test]
fn package_init_infers_safetensors_runtime_defaults_from_extension() {
let manifest = run_package_init_with_model("model.safetensors");
assert_eq!(manifest["model"]["format"], "safetensors");
assert_eq!(manifest["model"]["path"], "models/model.safetensors");
assert_eq!(manifest["runtime"]["backend"], "candle");
assert_eq!(manifest["runtime"]["target"], "local-cpu");
assert_eq!(manifest["runtime"]["version"], "candle.v0");
}
#[test]
fn package_init_writes_non_misleading_metadata_files() {
let temp = TempDir::new("package-init-metadata-files");
let model = temp.write("model.onnx", "model");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("init")
.arg(&output_dir)
.arg("--name")
.arg("protein-init")
.arg("--model")
.arg(&model)
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.arg("--license")
.arg("MIT")
.arg("--citation")
.arg("Smith et al. 2026")
.arg("--doi")
.arg("10.1234/example")
.arg("--model-card-summary")
.arg("Converted package fixture for CLI tests.")
.arg("--intended-use")
.arg("CLI conversion test")
.arg("--limitation")
.arg("Not for inference")
.output()
.expect("run biors package init");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());
let manifest: Value = serde_json::from_slice(
&std::fs::read(output_dir.join("manifest.json")).expect("read generated manifest"),
)
.expect("manifest JSON");
assert_eq!(
manifest["metadata"]["license"]["file"]["path"],
"docs/LICENSE-SPDX.txt"
);
assert_eq!(
manifest["metadata"]["citation"]["file"]["path"],
"docs/CITATION.txt"
);
assert_eq!(
std::fs::read_to_string(output_dir.join("docs/LICENSE-SPDX.txt")).expect("read license"),
"SPDX-License-Identifier: MIT\n"
);
assert_eq!(
std::fs::read_to_string(output_dir.join("docs/CITATION.txt")).expect("read citation"),
"Smith et al. 2026\nDOI: 10.1234/example\n"
);
assert!(!output_dir.join("docs/CITATION.cff").exists());
}
#[test]
fn package_init_rejects_unknown_model_extension() {
let temp = TempDir::new("package-init-unknown-model");
let model = temp.write("model.bin", "unknown model");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("--json")
.arg("package")
.arg("init")
.arg(&output_dir)
.arg("--name")
.arg("protein-init")
.arg("--model")
.arg(&model)
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.args(skeleton_metadata_args())
.output()
.expect("run biors package init");
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.init_unsupported_model_format"
);
assert!(value["error"]["location"]
.as_str()
.expect("error location")
.ends_with("model.bin"));
assert!(
!output_dir.join("manifest.json").exists(),
"unsupported model format must fail before writing manifest"
);
assert!(
!output_dir.exists(),
"unsupported model format must fail before creating package files"
);
}
#[test]
fn package_init_rejects_existing_generated_targets_without_force() {
for collision_rel in [
"models/model.onnx",
"fixtures/tiny.fasta",
"fixtures/tiny.output.json",
"tokenizers/protein-20.json",
"pipelines/protein.toml",
"docs/LICENSE-SPDX.txt",
"docs/CITATION.txt",
"docs/model-card.md",
] {
let temp = TempDir::new("package-init-collision");
let model = temp.write("model.onnx", "new model");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let collision_path = output_dir.join(collision_rel);
std::fs::create_dir_all(collision_path.parent().expect("collision parent"))
.expect("create collision parent");
std::fs::write(&collision_path, "existing").expect("write collision file");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("--json")
.arg("package")
.arg("init")
.arg(&output_dir)
.arg("--name")
.arg("protein-init")
.arg("--model")
.arg(&model)
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.args(skeleton_metadata_args())
.output()
.expect("run biors package init");
assert_eq!(output.status.code(), Some(2), "{collision_rel}");
assert!(output.stderr.is_empty(), "{collision_rel}");
let value: Value = serde_json::from_slice(&output.stdout).expect("valid JSON error");
assert_eq!(value["error"]["code"], "package.init_exists");
assert!(value["error"]["location"]
.as_str()
.expect("collision location")
.contains(collision_rel));
assert_eq!(
std::fs::read_to_string(&collision_path).expect("read collision file"),
"existing"
);
assert!(
!output_dir.join("manifest.json").exists(),
"package init should fail before writing manifest for {collision_rel}"
);
}
}
fn run_package_init_with_model(model_name: &str) -> Value {
let temp = TempDir::new("package-init-model-format");
let model = temp.write(model_name, "model");
let fixture_input = temp.write("tiny.fasta", ">tiny\nACDE\n");
let fixture_output = temp.write("tiny.output.json", r#"{"label":"fixture","score":1.0}"#);
let output_dir = temp.path().join("package");
let output = Command::new(env!("CARGO_BIN_EXE_biors"))
.arg("package")
.arg("init")
.arg(&output_dir)
.arg("--name")
.arg("protein-init")
.arg("--model")
.arg(&model)
.arg("--fixture-input")
.arg(&fixture_input)
.arg("--fixture-output")
.arg(&fixture_output)
.args(skeleton_metadata_args())
.output()
.expect("run biors package init");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(output.stderr.is_empty());
serde_json::from_slice(
&std::fs::read(output_dir.join("manifest.json")).expect("read generated manifest"),
)
.expect("generated manifest JSON")
}
fn conversion_metadata_args() -> [&'static str; 16] {
[
"--license",
"CC0-1.0",
"--citation",
"bio-rs converted fixture",
"--model-card",
"docs/model-card.md",
"--model-card-summary",
"Converted package fixture for CLI tests.",
"--intended-use",
"CLI conversion test",
"--limitation",
"Not for inference",
"--license-file",
"docs/LICENSE.txt",
"--citation-file",
"docs/CITATION.cff",
]
}
fn skeleton_metadata_args() -> [&'static str; 10] {
[
"--license",
"CC0-1.0",
"--citation",
"bio-rs converted fixture",
"--model-card-summary",
"Converted package fixture for CLI tests.",
"--intended-use",
"CLI conversion test",
"--limitation",
"Not for inference",
]
}