#![allow(clippy::expect_used)] #![allow(clippy::unwrap_used)] #![allow(clippy::indexing_slicing)] #![allow(clippy::arithmetic_side_effects)]
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn get_cli_binary() -> PathBuf {
let mut path = std::env::current_exe().expect("Failed to get test executable path");
path.pop(); path.pop(); path.push("aion");
path
}
fn run_cli(args: &[&str]) -> std::process::Output {
Command::new(get_cli_binary())
.args(args)
.output()
.expect("Failed to execute CLI")
}
fn run_cli_with_stdin(args: &[&str], stdin: &[u8]) -> std::process::Output {
use std::io::Write;
use std::process::Stdio;
let mut child = Command::new(get_cli_binary())
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to spawn CLI");
if let Some(ref mut stdin_handle) = child.stdin {
stdin_handle
.write_all(stdin)
.expect("Failed to write stdin");
}
child.wait_with_output().expect("Failed to wait on CLI")
}
fn setup_test_env() -> (TempDir, String) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key_id = format!("{}", rand::random::<u32>() % 900000 + 100000);
let output = run_cli(&["key", "generate", "--id", &key_id]);
assert!(
output.status.success(),
"Key generation failed: {}",
String::from_utf8_lossy(&output.stderr)
);
(temp_dir, key_id)
}
fn pin_author(registry_path: &std::path::Path, author_id: &str, key_id: &str) {
let output = run_cli(&[
"registry",
"pin",
"--author",
author_id,
"--key",
key_id,
"--output",
registry_path.to_str().expect("registry path utf8"),
]);
assert!(
output.status.success(),
"registry pin failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn cleanup_key(key_id: &str) {
let _ = run_cli(&["key", "delete", key_id, "--force"]);
}
#[test]
fn test_cli_help_displays_usage() {
let output = run_cli(&["--help"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("AION"));
assert!(stdout.contains("init"));
assert!(stdout.contains("commit"));
assert!(stdout.contains("verify"));
assert!(stdout.contains("show"));
assert!(stdout.contains("key"));
}
#[test]
fn test_cli_version_displays_version() {
let output = run_cli(&["--version"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("aion") || stdout.contains("0."));
}
#[test]
fn test_cli_subcommand_help() {
for subcommand in &["init", "commit", "verify", "show", "key"] {
let output = run_cli(&[subcommand, "--help"]);
assert!(
output.status.success(),
"{} help failed: {}",
subcommand,
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
#[ignore = "Requires keyring access which may prompt for password"]
fn test_cli_key_generate_and_list() {
let key_id = format!("{}", rand::random::<u32>() % 900000 + 200000);
let output = run_cli(&["key", "generate", "--id", &key_id]);
assert!(
output.status.success(),
"Key generate failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_cli(&["key", "list"]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(&key_id),
"Key list should contain {key_id}: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
#[ignore = "Requires interactive password input"]
fn test_cli_key_export_import_roundtrip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key_id = format!("{}", rand::random::<u32>() % 900000 + 300000);
let export_path = temp_dir.path().join("exported.key");
let output = run_cli(&["key", "generate", "--id", &key_id]);
assert!(output.status.success());
let output = Command::new(get_cli_binary())
.args([
"key",
"export",
&key_id,
"--output",
export_path.to_str().unwrap(),
])
.env("AION_KEY_PASSWORD", "testpassword123")
.output()
.expect("Export failed");
assert!(
output.status.success(),
"Key export failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(export_path.exists(), "Export file should exist");
run_cli(&["key", "delete", &key_id, "--force"]);
let new_key_id = format!("{}", rand::random::<u32>() % 900000 + 400000);
let output = Command::new(get_cli_binary())
.args([
"key",
"import",
export_path.to_str().unwrap(),
"--id",
&new_key_id,
])
.env("AION_KEY_PASSWORD", "testpassword123")
.output()
.expect("Import failed");
assert!(
output.status.success(),
"Key import failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_cli(&["key", "list"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains(&new_key_id));
cleanup_key(&new_key_id);
}
#[test]
fn test_cli_key_delete_requires_confirmation_or_force() {
let key_id = format!("{}", rand::random::<u32>() % 900000 + 500000);
run_cli(&["key", "generate", "--id", &key_id]);
let output = run_cli(&["key", "delete", &key_id, "--force"]);
assert!(output.status.success());
let output = run_cli(&["key", "list"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.contains(&key_id));
}
#[test]
fn test_cli_init_creates_file() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("test.aion");
let rules_path = temp_dir.path().join("rules.txt");
fs::write(&rules_path, "threshold: 100\nmode: strict").expect("Failed to write rules");
let output = run_cli(&[
"init",
file_path.to_str().unwrap(),
"--rules",
rules_path.to_str().unwrap(),
"--author",
"1001",
"--key",
&key_id,
"--message",
"Initial version",
]);
assert!(
output.status.success(),
"Init failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(file_path.exists(), "AION file should be created");
cleanup_key(&key_id);
}
#[test]
fn test_cli_init_with_stdin_rules() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("stdin_test.aion");
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"1002",
"--key",
&key_id,
"--message",
"From stdin",
],
b"rules from stdin",
);
assert!(
output.status.success(),
"Init with stdin failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(file_path.exists());
cleanup_key(&key_id);
}
#[test]
fn test_cli_init_fails_if_file_exists() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("exists.aion");
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"1003",
"--key",
&key_id,
],
b"initial",
);
assert!(output.status.success());
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"1003",
"--key",
&key_id,
],
b"second",
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("exists") || stderr.contains("already"),
"Should mention file exists: {stderr}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_commit_adds_version() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("commit_test.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "2001", &key_id);
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"2001",
"--key",
&key_id,
],
b"v1 rules",
);
assert!(output.status.success());
let output = run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"2001",
"--key",
&key_id,
"--message",
"Updated rules",
"--registry",
registry_path.to_str().unwrap(),
],
b"v2 rules",
);
assert!(
output.status.success(),
"Commit failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"info",
]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains('2') || stdout.contains("versions"),
"Should show 2 versions: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_commit_with_different_author() {
let (temp_dir, key_id1) = setup_test_env();
let key_id2 = format!("{}", rand::random::<u32>() % 900000 + 600000);
run_cli(&["key", "generate", "--id", &key_id2]);
let file_path = temp_dir.path().join("multi_author.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "3001", &key_id1);
pin_author(®istry_path, "3002", &key_id2);
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"3001",
"--key",
&key_id1,
],
b"author1 rules",
);
assert!(output.status.success());
let output = run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"3002",
"--key",
&key_id2,
"--message",
"Author 2 update",
"--registry",
registry_path.to_str().unwrap(),
],
b"author2 rules",
);
assert!(output.status.success());
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"history",
]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("3001") || stdout.contains("Author"));
assert!(stdout.contains("3002") || stdout.contains("Author"));
cleanup_key(&key_id1);
cleanup_key(&key_id2);
}
#[test]
fn test_cli_verify_valid_file() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("verify_test.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "4001", &key_id);
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"4001",
"--key",
&key_id,
],
b"test rules",
);
assert!(output.status.success());
let output = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.to_lowercase().contains("valid")
|| stdout.to_lowercase().contains("ok")
|| stdout.contains("✓")
|| stdout.contains("passed"),
"Should indicate valid: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_verify_verbose_output() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("verbose_verify.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "4002", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"4002",
"--key",
&key_id,
],
b"rules",
);
let output = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--verbose",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.len() > 50,
"Verbose output should be detailed: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_verify_corrupted_file_fails() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("corrupted.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "4003", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"4003",
"--key",
&key_id,
],
b"original rules",
);
let mut data = fs::read(&file_path).expect("Read failed");
if data.len() > 100 {
data[100] ^= 0xFF;
fs::write(&file_path, data).expect("Write failed");
}
let output = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"Tampered file must exit non-zero. exit={} stdout={} stderr={}",
output.status,
stdout,
stderr
);
assert!(
stdout.to_lowercase().contains("invalid"),
"stdout should report INVALID verdict, got: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_verify_exit_code_matches_verdict() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("exit_code_contract.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "4099", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"4099",
"--key",
&key_id,
],
b"contract rules",
);
let valid = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
valid.status.success(),
"Valid file must exit 0, got {}",
valid.status
);
assert_eq!(valid.status.code(), Some(0));
let mut data = fs::read(&file_path).expect("Read failed");
let mid = data.len() / 2;
data[mid] ^= 0xFF;
fs::write(&file_path, data).expect("Write failed");
let tampered = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
!tampered.status.success(),
"Tampered file must exit non-zero, got {}",
tampered.status
);
assert_eq!(
tampered.status.code(),
Some(1),
"Tampered file must exit 1, got {:?}",
tampered.status.code()
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_show_rules() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("show_rules.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "5001", &key_id);
let rules_content = "threshold: 500\nmode: relaxed";
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"5001",
"--key",
&key_id,
],
rules_content.as_bytes(),
);
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"rules",
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("threshold") || stdout.contains("500"),
"Should show rules content: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_show_history() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("show_history.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "5002", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"5002",
"--key",
&key_id,
"--message",
"First commit",
],
b"v1",
);
run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"5002",
"--key",
&key_id,
"--message",
"Second commit",
"--registry",
registry_path.to_str().unwrap(),
],
b"v2",
);
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"history",
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("First") || stdout.contains('1'));
assert!(stdout.contains("Second") || stdout.contains('2'));
cleanup_key(&key_id);
}
#[test]
fn test_cli_show_signatures() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("show_sigs.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "5003", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"5003",
"--key",
&key_id,
],
b"rules",
);
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"signatures",
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.len() > 10, "Should show signature info: {stdout}");
cleanup_key(&key_id);
}
#[test]
fn test_cli_show_info() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("show_info.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "5004", &key_id);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"5004",
"--key",
&key_id,
],
b"rules",
);
let output = run_cli(&[
"show",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"info",
]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("version") || stdout.contains("author") || stdout.contains("5004"),
"Should show file info: {stdout}"
);
cleanup_key(&key_id);
}
#[test]
fn test_cli_commit_rejects_unregistered_signer() {
let (temp_dir, legit_key) = setup_test_env();
let rogue_key = format!("{}", rand::random::<u32>() % 900000 + 700000);
run_cli(&["key", "generate", "--id", &rogue_key]);
let file_path = temp_dir.path().join("precheck.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "6001", &legit_key);
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"6001",
"--key",
&legit_key,
],
b"v1",
);
assert!(output.status.success(), "init failed");
let pre_commit_bytes = fs::read(&file_path).expect("read pre-commit");
let output = run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"6999",
"--key",
&rogue_key,
"--message",
"rogue amendment",
"--registry",
registry_path.to_str().unwrap(),
],
b"rogue rules",
);
assert!(
!output.status.success(),
"unregistered commit must exit non-zero, got {}",
output.status
);
let post_commit_bytes = fs::read(&file_path).expect("read post-commit");
assert_eq!(
pre_commit_bytes, post_commit_bytes,
"refused commit must not mutate the file"
);
cleanup_key(&legit_key);
cleanup_key(&rogue_key);
}
#[test]
fn test_cli_commit_rejects_wrong_operational_key() {
let (temp_dir, pinned_key) = setup_test_env();
let other_key = format!("{}", rand::random::<u32>() % 900000 + 800000);
run_cli(&["key", "generate", "--id", &other_key]);
let file_path = temp_dir.path().join("wrongkey.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "6002", &pinned_key);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"6002",
"--key",
&pinned_key,
],
b"v1",
);
let pre_commit_bytes = fs::read(&file_path).expect("read pre-commit");
let output = run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"6002",
"--key",
&other_key,
"--message",
"wrong key",
"--registry",
registry_path.to_str().unwrap(),
],
b"v2",
);
assert!(
!output.status.success(),
"wrong operational key must exit non-zero, got {}",
output.status
);
let post_commit_bytes = fs::read(&file_path).expect("read post-commit");
assert_eq!(
pre_commit_bytes, post_commit_bytes,
"refused commit must not mutate the file"
);
cleanup_key(&pinned_key);
cleanup_key(&other_key);
}
#[test]
fn test_cli_commit_force_unregistered_bypasses() {
let (temp_dir, legit_key) = setup_test_env();
let rogue_key = format!("{}", rand::random::<u32>() % 900000 + 850000);
run_cli(&["key", "generate", "--id", &rogue_key]);
let file_path = temp_dir.path().join("force.aion");
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "6003", &legit_key);
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"6003",
"--key",
&legit_key,
],
b"v1",
);
let output = run_cli_with_stdin(
&[
"commit",
file_path.to_str().unwrap(),
"--author",
"6999",
"--key",
&rogue_key,
"--message",
"force write",
"--registry",
registry_path.to_str().unwrap(),
"--force-unregistered",
],
b"forced rules",
);
assert!(
output.status.success(),
"--force-unregistered commit must succeed, got {}",
output.status
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("force-unregistered") || stderr.contains("⚠️"),
"stderr must carry the bypass warning, got: {stderr}"
);
let verify = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
!verify.status.success(),
"file written with --force-unregistered must fail verify"
);
cleanup_key(&legit_key);
cleanup_key(&rogue_key);
}
fn pin_with_distinct_master(
registry_path: &std::path::Path,
author_id: &str,
op_key_id: &str,
master_key_id: &str,
) {
let output = run_cli(&[
"registry",
"pin",
"--author",
author_id,
"--key",
op_key_id,
"--master",
master_key_id,
"--output",
registry_path.to_str().expect("utf8"),
]);
assert!(
output.status.success(),
"registry pin (with master) failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_cli_registry_rotate_happy_path() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 270000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 370000);
run_cli(&["key", "generate", "--id", &master_key]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "7001", &op_key, &master_key);
let file_path = temp_dir.path().join("before.aion");
run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"7001",
"--key",
&op_key,
],
b"before rotation",
);
let output = run_cli(&[
"registry",
"rotate",
"--author",
"7001",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"5",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
output.status.success(),
"rotate failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let verify_old = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
verify_old.status.success(),
"pre-rotation file must still verify after rotation, got {}",
verify_old.status
);
cleanup_key(&op_key);
cleanup_key(&master_key);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_registry_rotate_warns_on_retroactive_invalidation() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 950000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 960000);
run_cli(&["key", "generate", "--id", &master_key]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "9501", &op_key, &master_key);
let output = run_cli(&[
"registry",
"rotate",
"--author",
"9501",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"0",
"--registry",
registry_path.to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--effective-from-version 0") || stderr.contains("window collapses"),
"rotate must warn on retroactive invalidation, got stderr: {stderr}"
);
let registry_clean = temp_dir.path().join("registry-clean.json");
pin_with_distinct_master(®istry_clean, "9502", &op_key, &master_key);
let quiet = run_cli(&[
"registry",
"rotate",
"--author",
"9502",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"5",
"--registry",
registry_clean.to_str().unwrap(),
"--no-warn",
]);
assert!(quiet.status.success(), "clean rotation must succeed");
let quiet_stderr = String::from_utf8_lossy(&quiet.stderr);
assert!(
!quiet_stderr.contains("window collapses"),
"--no-warn must suppress the warning, got stderr: {quiet_stderr}"
);
cleanup_key(&op_key);
cleanup_key(&master_key);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_registry_rotate_no_warning_on_clean_rotation() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 970000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 980000);
run_cli(&["key", "generate", "--id", &master_key]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "9503", &op_key, &master_key);
let output = run_cli(&[
"registry",
"rotate",
"--author",
"9503",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"10",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(output.status.success(), "clean rotation must succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("window collapses"),
"no warning expected for V > created_at, got: {stderr}"
);
cleanup_key(&op_key);
cleanup_key(&master_key);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_registry_rotate_rejects_wrong_master() {
let (temp_dir, op_key) = setup_test_env();
let real_master = format!("{}", rand::random::<u32>() % 900000 + 280000);
let fake_master = format!("{}", rand::random::<u32>() % 900000 + 380000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 480000);
run_cli(&["key", "generate", "--id", &real_master]);
run_cli(&["key", "generate", "--id", &fake_master]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "7002", &op_key, &real_master);
let pre_bytes = fs::read(®istry_path).expect("read pre");
let output = run_cli(&[
"registry",
"rotate",
"--author",
"7002",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&fake_master,
"--effective-from-version",
"5",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
!output.status.success(),
"wrong-master rotation must fail, got {}",
output.status
);
let post_bytes = fs::read(®istry_path).expect("read post");
assert_eq!(
pre_bytes, post_bytes,
"failed rotation must not mutate the registry file"
);
cleanup_key(&op_key);
cleanup_key(&real_master);
cleanup_key(&fake_master);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_registry_revoke_happy_path() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 290000);
run_cli(&["key", "generate", "--id", &master_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "7003", &op_key, &master_key);
let pre_file = temp_dir.path().join("pre.aion");
run_cli_with_stdin(
&[
"init",
pre_file.to_str().unwrap(),
"--author",
"7003",
"--key",
&op_key,
],
b"before revoke",
);
let output = run_cli(&[
"registry",
"revoke",
"--author",
"7003",
"--epoch",
"0",
"--reason",
"compromised",
"--master-key",
&master_key,
"--effective-from-version",
"3",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
output.status.success(),
"revoke failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let verify_pre = run_cli(&[
"verify",
pre_file.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
verify_pre.status.success(),
"pre-revocation file must still verify, got {}",
verify_pre.status
);
cleanup_key(&op_key);
cleanup_key(&master_key);
}
fn seal_minimal_release(
bundle_dir: &std::path::Path,
primary_path: &std::path::Path,
author_id: &str,
key_id: &str,
aion_version: u64,
) -> std::process::Output {
run_cli(&[
"release",
"seal",
"--primary",
primary_path.to_str().unwrap(),
"--primary-name",
"model.safetensors",
"--model-name",
"cirrus-7b-safety",
"--model-version",
"0.3.1",
"--model-format",
"safetensors",
"--framework",
"pytorch:2.3.1",
"--license",
"Apache-2.0:weights",
"--safety-attestation",
"rlhf_alignment:PASS",
"--export-control",
"US-EAR:EAR99",
"--builder-id",
"https://ci.example/run/1",
"--aion-version",
&aion_version.to_string(),
"--author",
author_id,
"--key",
key_id,
"--out-dir",
bundle_dir.to_str().unwrap(),
])
}
#[test]
fn test_cli_release_seal_verify_roundtrip() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 410000);
run_cli(&["key", "generate", "--id", &master_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "8001", &op_key, &master_key);
let primary = temp_dir.path().join("model.bin");
fs::write(&primary, vec![0xAAu8; 4096]).unwrap();
let bundle_dir = temp_dir.path().join("bundle");
let seal = seal_minimal_release(&bundle_dir, &primary, "8001", &op_key, 1);
assert!(
seal.status.success(),
"seal failed: {}",
String::from_utf8_lossy(&seal.stderr)
);
assert!(bundle_dir.join("release.json").exists());
assert!(bundle_dir.join("primary.bin").exists());
let verify = run_cli(&[
"release",
"verify",
"--bundle",
bundle_dir.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--at-version",
"1",
]);
assert!(
verify.status.success(),
"verify must exit 0 on valid bundle, got {}",
verify.status
);
assert_eq!(verify.status.code(), Some(0));
cleanup_key(&op_key);
cleanup_key(&master_key);
}
#[test]
fn test_cli_release_verify_rejects_primary_tamper() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 420000);
run_cli(&["key", "generate", "--id", &master_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "8002", &op_key, &master_key);
let primary = temp_dir.path().join("model.bin");
fs::write(&primary, vec![0xBBu8; 2048]).unwrap();
let bundle_dir = temp_dir.path().join("bundle");
let seal = seal_minimal_release(&bundle_dir, &primary, "8002", &op_key, 1);
assert!(seal.status.success(), "seal failed");
let release_json = bundle_dir.join("release.json");
let mut contents = fs::read_to_string(&release_json).unwrap();
let marker = "\"manifest_canonical_hex\": \"";
let start = contents.find(marker).unwrap() + marker.len();
let mut bytes = contents.clone().into_bytes();
bytes[start + 4] = if bytes[start + 4] == b'0' { b'1' } else { b'0' };
contents = String::from_utf8(bytes).unwrap();
fs::write(&release_json, contents).unwrap();
let verify = run_cli(&[
"release",
"verify",
"--bundle",
bundle_dir.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--at-version",
"1",
]);
assert!(
!verify.status.success(),
"tampered manifest must fail verify, got {}",
verify.status
);
cleanup_key(&op_key);
cleanup_key(&master_key);
}
#[test]
fn test_cli_release_verify_rejects_rotated_out_key() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 430000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 530000);
run_cli(&["key", "generate", "--id", &master_key]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "8003", &op_key, &master_key);
let primary = temp_dir.path().join("model.bin");
fs::write(&primary, vec![0xCCu8; 1024]).unwrap();
let bundle_dir = temp_dir.path().join("bundle");
let seal = seal_minimal_release(&bundle_dir, &primary, "8003", &op_key, 1);
assert!(seal.status.success(), "seal failed");
let verify_v1 = run_cli(&[
"release",
"verify",
"--bundle",
bundle_dir.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--at-version",
"1",
]);
assert!(
verify_v1.status.success(),
"original-version verify must pass, got {}",
verify_v1.status
);
let rotate = run_cli(&[
"registry",
"rotate",
"--author",
"8003",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"5",
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(rotate.status.success(), "rotate failed");
let verify_v10 = run_cli(&[
"release",
"verify",
"--bundle",
bundle_dir.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--at-version",
"10",
]);
assert!(
!verify_v10.status.success(),
"post-rotation verify must fail (rotated-out key), got {}",
verify_v10.status
);
cleanup_key(&op_key);
cleanup_key(&master_key);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_release_inspect_prints_summary() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 440000);
run_cli(&["key", "generate", "--id", &master_key]);
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "8004", &op_key, &master_key);
let primary = temp_dir.path().join("model.bin");
fs::write(&primary, vec![0xDDu8; 512]).unwrap();
let bundle_dir = temp_dir.path().join("bundle");
let seal = seal_minimal_release(&bundle_dir, &primary, "8004", &op_key, 1);
assert!(seal.status.success());
let inspect = run_cli(&[
"release",
"inspect",
"--bundle",
bundle_dir.to_str().unwrap(),
]);
assert!(inspect.status.success());
let stdout = String::from_utf8_lossy(&inspect.stdout);
assert!(
stdout.contains("cirrus-7b-safety") && stdout.contains("0.3.1"),
"inspect must print model name/version, got: {stdout}"
);
cleanup_key(&op_key);
cleanup_key(&master_key);
}
#[test]
fn test_cli_archive_verify_finds_planted_anomalies() {
let (temp_dir, op_key) = setup_test_env();
let other_key = format!("{}", rand::random::<u32>() % 900000 + 920000);
run_cli(&["key", "generate", "--id", &other_key]);
let archive = temp_dir.path().join("archive");
fs::create_dir_all(&archive).unwrap();
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "9001", &op_key);
for i in 1..=5u32 {
let p = archive.join(format!("week-{i:02}.aion"));
run_cli_with_stdin(
&[
"init",
p.to_str().unwrap(),
"--author",
"9001",
"--key",
&op_key,
],
format!("rules week {i}").as_bytes(),
);
}
let week3 = archive.join("week-03.aion");
let mut data = fs::read(&week3).unwrap();
let target = (data.len() - 32) * 6 / 10;
data[target] ^= 0x5A;
fs::write(&week3, data).unwrap();
let other_path = archive.join("week-06-rogue.aion");
run_cli_with_stdin(
&[
"init",
other_path.to_str().unwrap(),
"--author",
"9999",
"--key",
&other_key,
],
b"rogue rules",
);
let output = run_cli(&[
"archive",
"verify",
archive.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--format",
"json",
]);
assert!(
!output.status.success(),
"archive verify must exit non-zero when any file is invalid"
);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("\"file_count\": 6"),
"expected 6 files counted, got: {stdout}"
);
assert!(
stdout.contains("\"valid_count\": 4"),
"expected 4 valid (5 clean - 1 tampered + 0 rogue), got: {stdout}"
);
assert!(
stdout.contains("\"invalid_count\": 2"),
"expected 2 invalid (week-03 tamper + week-06-rogue), got: {stdout}"
);
assert!(
stdout.contains("week-03.aion") && stdout.contains("integrity"),
"expected integrity error on week-03, got: {stdout}"
);
assert!(
stdout.contains("week-06-rogue.aion"),
"expected rogue file flagged, got: {stdout}"
);
cleanup_key(&op_key);
cleanup_key(&other_key);
}
#[test]
fn test_cli_archive_verify_clean_archive_exits_zero() {
let (temp_dir, op_key) = setup_test_env();
let archive = temp_dir.path().join("archive");
fs::create_dir_all(&archive).unwrap();
let registry_path = temp_dir.path().join("registry.json");
pin_author(®istry_path, "9002", &op_key);
for i in 1..=3u32 {
let p = archive.join(format!("file-{i}.aion"));
run_cli_with_stdin(
&[
"init",
p.to_str().unwrap(),
"--author",
"9002",
"--key",
&op_key,
],
format!("rules {i}").as_bytes(),
);
}
let output = run_cli(&[
"archive",
"verify",
archive.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
output.status.success(),
"clean archive must exit 0, got {}",
output.status
);
assert_eq!(output.status.code(), Some(0));
cleanup_key(&op_key);
}
#[test]
fn test_cli_archive_verify_detects_rotation() {
let (temp_dir, op_key) = setup_test_env();
let master_key = format!("{}", rand::random::<u32>() % 900000 + 930000);
let new_op_key = format!("{}", rand::random::<u32>() % 900000 + 940000);
run_cli(&["key", "generate", "--id", &master_key]);
run_cli(&["key", "generate", "--id", &new_op_key]);
let archive = temp_dir.path().join("archive");
fs::create_dir_all(&archive).unwrap();
let registry_path = temp_dir.path().join("registry.json");
pin_with_distinct_master(®istry_path, "9003", &op_key, &master_key);
for i in 1..=2u32 {
let p = archive.join(format!("pre-{i}.aion"));
run_cli_with_stdin(
&[
"init",
p.to_str().unwrap(),
"--author",
"9003",
"--key",
&op_key,
],
format!("pre {i}").as_bytes(),
);
}
run_cli(&[
"registry",
"rotate",
"--author",
"9003",
"--from-epoch",
"0",
"--to-epoch",
"1",
"--new-key",
&new_op_key,
"--master-key",
&master_key,
"--effective-from-version",
"2",
"--registry",
registry_path.to_str().unwrap(),
]);
let post = archive.join("post-1.aion");
run_cli_with_stdin(
&[
"init",
post.to_str().unwrap(),
"--author",
"9003",
"--key",
&new_op_key,
],
b"post",
);
let output = run_cli(&[
"archive",
"verify",
archive.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
"--format",
"json",
]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("\"rotated\": true"),
"rotation must be reported, got: {stdout}"
);
assert!(
stdout.contains("\"author\": 9003"),
"author 9003 must appear in breakdown, got: {stdout}"
);
cleanup_key(&op_key);
cleanup_key(&master_key);
cleanup_key(&new_op_key);
}
#[test]
fn test_cli_invalid_subcommand_fails() {
let output = run_cli(&["nonexistent"]);
assert!(!output.status.success());
}
#[test]
fn test_cli_missing_required_args_fails() {
let output = run_cli(&["init"]);
assert!(!output.status.success());
let output = run_cli(&["commit"]);
assert!(!output.status.success());
}
#[test]
fn test_cli_verify_nonexistent_file_fails() {
let output = run_cli(&["verify", "/nonexistent/path/file.aion"]);
assert!(!output.status.success());
}
#[test]
fn test_cli_show_nonexistent_file_fails() {
let output = run_cli(&["show", "/nonexistent/path/file.aion", "info"]);
assert!(!output.status.success());
}
#[test]
fn test_cli_commit_nonexistent_file_fails() {
let key_id = format!("{}", rand::random::<u32>() % 900000 + 700000);
run_cli(&["key", "generate", "--id", &key_id]);
let output = run_cli_with_stdin(
&[
"commit",
"/nonexistent/path/file.aion",
"--author",
"9999",
"--key",
&key_id,
"--message",
"test",
],
b"rules",
);
assert!(!output.status.success());
cleanup_key(&key_id);
}
#[test]
fn test_cli_init_with_nonexistent_key_fails() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = temp_dir.path().join("test.aion");
let output = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
"1000",
"--key",
"nonexistent_key_12345",
],
b"rules",
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("key") || stderr.contains("not found") || stderr.contains("error"),
"Should indicate key error: {stderr}"
);
}
fn extract_public_key_hex(stdout: &str) -> [u8; 32] {
let line = stdout
.lines()
.find(|line| line.trim_start().starts_with("Public Key:"))
.expect("Public Key line missing from stdout");
let hex_str = line
.split(':')
.nth(1)
.expect("Public Key line has no ':'")
.trim();
let bytes = hex::decode(hex_str).expect("Public Key hex decode failed");
<[u8; 32]>::try_from(bytes.as_slice()).expect("Public Key not 32 bytes")
}
fn build_registry_json(author_id: u64, public_key: &[u8; 32]) -> String {
use base64::Engine;
let encoded = base64::engine::general_purpose::STANDARD.encode(public_key);
format!(
r#"{{"version":1,"authors":[{{
"author_id": {author_id},
"master_key": "{encoded}",
"epochs": [{{"epoch":0,"public_key":"{encoded}","active_from_version":0}}]
}}]}}"#
)
}
#[test]
fn test_verify_with_registry_accepts_matching_key() {
let (temp_dir, key_id) = setup_test_env();
let inspect_id = format!("{}", rand::random::<u32>() % 900_000 + 100_000);
let gen_output = run_cli(&["key", "generate", "--id", &inspect_id]);
assert!(
gen_output.status.success(),
"key generate for inspect_id failed: {}",
String::from_utf8_lossy(&gen_output.stderr)
);
let stdout = String::from_utf8_lossy(&gen_output.stdout);
let public_key = extract_public_key_hex(&stdout);
let inspect_author: u64 = inspect_id.parse().expect("inspect_id parses");
let file_path = temp_dir.path().join("registry_verify.aion");
let init_out = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
&inspect_id,
"--key",
&inspect_id,
"--message",
"genesis",
],
b"some rules",
);
assert!(
init_out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init_out.stderr)
);
let registry_path = temp_dir.path().join("registry.json");
fs::write(
®istry_path,
build_registry_json(inspect_author, &public_key),
)
.expect("registry write");
let verify_out = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
verify_out.status.success(),
"verify-with-registry should succeed: stdout={} stderr={}",
String::from_utf8_lossy(&verify_out.stdout),
String::from_utf8_lossy(&verify_out.stderr)
);
let stdout = String::from_utf8_lossy(&verify_out.stdout);
assert!(
stdout.contains("VALID") || stdout.contains("Registry"),
"verify output should indicate success: {stdout}"
);
cleanup_key(&inspect_id);
cleanup_key(&key_id);
drop(temp_dir);
}
#[test]
fn test_verify_with_registry_rejects_mismatched_key() {
let (temp_dir, key_id) = setup_test_env();
let inspect_id = format!("{}", rand::random::<u32>() % 900_000 + 100_000);
let gen_output = run_cli(&["key", "generate", "--id", &inspect_id]);
assert!(gen_output.status.success());
let inspect_author: u64 = inspect_id.parse().expect("inspect_id parses");
let file_path = temp_dir.path().join("registry_reject.aion");
let init_out = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
&inspect_id,
"--key",
&inspect_id,
"--message",
"genesis",
],
b"some rules",
);
assert!(init_out.status.success());
let wrong_pubkey = [0xAB_u8; 32];
let registry_path = temp_dir.path().join("registry.json");
fs::write(
®istry_path,
build_registry_json(inspect_author, &wrong_pubkey),
)
.expect("registry write");
let verify_out = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
!verify_out.status.success(),
"verify should FAIL under a wrong-key registry; stdout={} stderr={}",
String::from_utf8_lossy(&verify_out.stdout),
String::from_utf8_lossy(&verify_out.stderr)
);
cleanup_key(&inspect_id);
cleanup_key(&key_id);
drop(temp_dir);
}
#[test]
fn test_verify_rejects_malformed_registry() {
let (temp_dir, key_id) = setup_test_env();
let file_path = temp_dir.path().join("ignored.aion");
let init_out = run_cli_with_stdin(
&[
"init",
file_path.to_str().unwrap(),
"--author",
&key_id,
"--key",
&key_id,
"--message",
"genesis",
],
b"some rules",
);
assert!(init_out.status.success());
let registry_path = temp_dir.path().join("bad.json");
fs::write(®istry_path, "not valid json {[}").expect("registry write");
let verify_out = run_cli(&[
"verify",
file_path.to_str().unwrap(),
"--registry",
registry_path.to_str().unwrap(),
]);
assert!(
!verify_out.status.success(),
"malformed registry must produce a non-zero exit"
);
let stderr = String::from_utf8_lossy(&verify_out.stderr);
assert!(
stderr.contains("parse") || stderr.contains("registry") || stderr.contains("error"),
"stderr should reference the parse failure: {stderr}"
);
cleanup_key(&key_id);
drop(temp_dir);
}