use std::{ fs, io::{Read, Write}, net::TcpListener, process::Command, thread, }; use serde_json::Value; use tempfile::TempDir; fn fixture() -> (TempDir, std::path::PathBuf) { let root = tempfile::tempdir().unwrap(); fs::write(root.path().join("smoke.prg"), [0x01, 0x08, 0xea]).unwrap(); let manifest = root.path().join("c64dev.toml"); fs::write( &manifest, r#" schema_version = 3 [project] name = "cli-test" [build] command = ["true"] working_directory = "." artifact = "smoke.prg" toolchain = "test" [diagnostics] address = 49152 length = 256 command_address = 49408 screen_address = 1024 color_address = 55296 [vice] executable = "x64sc" poll_interval_ms = 1 [ultimate] poll_interval_ms = 1 request_timeout_ms = 100 [[tests]] name = "smoke" timeout_ms = 2000 heartbeat_timeout_ms = 100 expected_terminal = "pass" expected_screen = "ULTIMATE TOOL" startup_command = { name = "scenario", payload = "pass" } "#, ) .unwrap(); (root, manifest) } fn cli() -> Command { Command::new(env!("CARGO_BIN_EXE_ultimate-tool")) } #[test] fn debug_capture_requires_a_pinned_identity_before_network_io() { let (_root, manifest) = fixture(); let output = cli() .args([ "--format", "json", "capture", "debug-stream", "--manifest", manifest.to_str().unwrap(), "--device", "192.0.2.10", "--interface", "192.0.2.20", "--confirm-stream-start-count", "1", "--confirm-stream-stop-count", "1", ]) .output() .unwrap(); assert!(!output.status.success()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("expected device ID or hostname") ); } #[test] fn source_linked_capture_requires_exact_counts_before_build_or_network_io() { let (_root, manifest) = fixture(); let output = cli() .args([ "--format", "json", "capture", "source-linked", "--manifest", manifest.to_str().unwrap(), "--test", "timeline-probe", "--device", "192.0.2.10", "--interface", "192.0.2.20", "--expect-device-id", "ABC123", "--confirm-reset-count", "0", "--confirm-stream-start-count", "1", "--confirm-stream-stop-count", "1", ]) .output() .unwrap(); assert_eq!(output.status.code(), Some(2)); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("--confirm-reset-count 1") ); } fn executable_riscv32_elf() -> Vec { const ELF_HEADER_SIZE: usize = 52; const PROGRAM_HEADER_SIZE: usize = 32; const PAYLOAD_OFFSET: usize = 0x100; const LOAD_ADDRESS: u32 = 0x0300_0000; const PT_LOAD: u32 = 1; const PF_X: u32 = 1; const PF_R: u32 = 4; let payload = [0x13, 0, 0, 0]; let mut bytes = vec![0_u8; PAYLOAD_OFFSET + payload.len()]; bytes[0..4].copy_from_slice(b"\x7fELF"); bytes[4] = 1; bytes[5] = 1; bytes[6] = 1; bytes[16..18].copy_from_slice(&2_u16.to_le_bytes()); bytes[18..20].copy_from_slice(&243_u16.to_le_bytes()); bytes[20..24].copy_from_slice(&1_u32.to_le_bytes()); bytes[24..28].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[28..32].copy_from_slice(&(ELF_HEADER_SIZE as u32).to_le_bytes()); bytes[40..42].copy_from_slice(&(ELF_HEADER_SIZE as u16).to_le_bytes()); bytes[42..44].copy_from_slice(&(PROGRAM_HEADER_SIZE as u16).to_le_bytes()); bytes[44..46].copy_from_slice(&1_u16.to_le_bytes()); let program = ELF_HEADER_SIZE; bytes[program..program + 4].copy_from_slice(&PT_LOAD.to_le_bytes()); bytes[program + 4..program + 8].copy_from_slice(&(PAYLOAD_OFFSET as u32).to_le_bytes()); bytes[program + 8..program + 12].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[program + 12..program + 16].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[program + 16..program + 20].copy_from_slice(&(payload.len() as u32).to_le_bytes()); bytes[program + 20..program + 24].copy_from_slice(&8_u32.to_le_bytes()); bytes[program + 24..program + 28].copy_from_slice(&(PF_R | PF_X).to_le_bytes()); bytes[program + 28..program + 32].copy_from_slice(&4_u32.to_le_bytes()); bytes[PAYLOAD_OFFSET..].copy_from_slice(&payload); bytes } fn native_fixture() -> (TempDir, std::path::PathBuf) { let root = tempfile::tempdir().unwrap(); fs::write(root.path().join("input.elf"), executable_riscv32_elf()).unwrap(); fs::write(root.path().join("native.map"), "0x03000000 _start\n").unwrap(); let manifest = root.path().join("c64dev.toml"); fs::write( &manifest, r#" schema_version = 10 [project] name = "cli-native-test" kind = "ultimate_riscv" [build] command = ["sh", "-c", "cp input.elf native.elf"] working_directory = "." artifact = "native.elf" supporting_artifacts = ["native.map"] toolchain = "test" [ultimate] poll_interval_ms = 50 request_timeout_ms = 100 [native] protocol = "utr2" timeout_ms = 1000 heartbeat_timeout_ms = 200 "#, ) .unwrap(); (root, manifest) } #[test] fn ultimate_probe_returns_clean_json_without_async_runtime_panic() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); let server = thread::spawn(move || { for _ in 0..2 { let (mut stream, _) = listener.accept().unwrap(); let mut request = [0_u8; 2048]; let length = stream.read(&mut request).unwrap(); let request = String::from_utf8_lossy(&request[..length]); let body = if request.starts_with("GET /v1/version ") { r#"{"version":"1.0"}"# } else if request.starts_with("GET /v1/info ") { r#"{"product":"C64 Ultimate","hostname":"test-c64","unique_id":"ABC123","firmware_version":"1.1.0","fpga_version":"122","core_version":"1.49"}"# } else { panic!("unexpected request: {request}"); }; write!( stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) .unwrap(); } }); let output = cli() .args([ "--format", "json", "probe", "ultimate", "--device", &address.to_string(), ]) .output() .unwrap(); server.join().unwrap(); assert!( output.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["data"]["unique_id"], "ABC123"); } #[test] fn json_result_and_error_envelopes_keep_stdout_clean() { let (_root, manifest) = fixture(); let success = cli() .args(["--format", "json", "validate", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(success.status.success()); assert!(success.stderr.is_empty()); let lines = String::from_utf8(success.stdout).unwrap(); assert_eq!(lines.lines().count(), 1); let value: Value = serde_json::from_str(lines.trim()).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["command"], "validate"); let missing = manifest.with_file_name("missing.toml"); let failure = cli() .args(["--format", "json", "validate", "--manifest"]) .arg(missing) .output() .unwrap(); assert_eq!(failure.status.code(), Some(2)); assert!(failure.stderr.is_empty()); let value: Value = serde_json::from_slice(&failure.stdout).unwrap(); assert_eq!(value["record_kind"], "error"); assert_eq!(value["error"]["code"], "configuration_invalid"); assert_eq!(value["error"]["exit_code"], 2); } #[test] fn ue2_pack_inspect_validate_and_no_overwrite_use_versioned_cli() { let root = tempfile::tempdir().unwrap(); let elf = root.path().join("native.elf"); let ue2 = root.path().join("native.ue2"); fs::write(&elf, executable_riscv32_elf()).unwrap(); let packed = cli() .args(["--format", "json", "ue2", "pack", "--elf"]) .arg(&elf) .arg("--output") .arg(&ue2) .output() .unwrap(); assert!( packed.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&packed.stderr), String::from_utf8_lossy(&packed.stdout) ); assert!(packed.stderr.is_empty()); let value: Value = serde_json::from_slice(&packed.stdout).unwrap(); assert_eq!(value["schema_version"], 1); assert_eq!(value["command"], "ue2.pack"); assert_eq!(value["data"]["validation"]["policy"], "u64ii-volatile-v1"); assert_eq!( value["data"]["validation"]["inspection"]["entry_point"], 0x0300_0000 ); for arguments in [ vec!["ue2", "inspect", ue2.to_str().unwrap()], vec![ "ue2", "validate", ue2.to_str().unwrap(), "--elf", elf.to_str().unwrap(), ], ] { let output = cli() .args(["--format", "json"]) .args(arguments) .output() .unwrap(); assert!(output.status.success()); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["data"]["policy"], "u64ii-volatile-v1"); } let no_overwrite = cli() .args(["--format", "json", "ue2", "pack", "--elf"]) .arg(&elf) .arg("--output") .arg(&ue2) .output() .unwrap(); assert_eq!(no_overwrite.status.code(), Some(2)); assert!(no_overwrite.stderr.is_empty()); let value: Value = serde_json::from_slice(&no_overwrite.stdout).unwrap(); assert_eq!(value["error"]["code"], "artifact_invalid"); } #[test] fn native_prepare_creates_an_immutable_hash_bound_bundle() { let (root, manifest) = native_fixture(); let output = cli() .args(["--format", "json", "riscv", "prepare", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(output.status.success(), "{:?}", output); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["command"], "riscv.prepare"); assert_eq!(value["data"]["project"], "cli-native-test"); assert_eq!( value["data"]["post_recovery_c64_health_check"]["sha256"], "dac6abb2ea01752e94c4318463efa6c54942713ffb021b67f1a4e1d66fa02560" ); let preparation_id = value["data"]["preparation_id"].as_str().unwrap(); let directory = root .path() .join(".ultimate-tool/preparations") .join(preparation_id); assert!(directory.join("application.elf").is_file()); assert!(directory.join("application.ue2").is_file()); assert!(directory.join("preparation.json").is_file()); assert!(directory.join("SHA256SUMS").is_file()); } #[test] fn native_standing_intent_probe_does_not_drop_blocking_client_in_async_runtime() { let (root, manifest) = native_fixture(); let prepared = cli() .args(["--format", "json", "riscv", "prepare", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(prepared.status.success(), "{prepared:?}"); let prepared: Value = serde_json::from_slice(&prepared.stdout).unwrap(); let preparation_id = prepared["data"]["preparation_id"].as_str().unwrap(); let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); let server = thread::spawn(move || { for _ in 0..2 { let (mut stream, _) = listener.accept().unwrap(); let mut request = [0_u8; 2048]; let length = stream.read(&mut request).unwrap(); let request = String::from_utf8_lossy(&request[..length]); let body = if request.starts_with("GET /v1/version ") { r#"{"version":"1.0"}"# } else if request.starts_with("GET /v1/info ") { r#"{"product":"C64 Ultimate","hostname":"test-c64","unique_id":"ABC123","firmware_version":"1.1.0","fpga_version":"122","core_version":"1.49"}"# } else { panic!("unexpected request: {request}"); }; write!( stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) .unwrap(); } }); let output = cli() .args([ "--format", "json", "riscv", "intent", "create", "--manifest", ]) .arg(&manifest) .args([ "--preparation", preparation_id, "--device", &address.to_string(), "--expect-device-id", "ABC123", "--authorization", "standing", "--confirm-standing-native-execution", ]) .output() .unwrap(); server.join().unwrap(); assert!( output.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["command"], "riscv.intent.create"); assert_eq!(value["data"]["authorization"], "standing"); assert_eq!(value["data"]["use_count"], 0); let intent_id = value["data"]["intent_id"].as_str().unwrap(); assert!( root.path() .join(".ultimate-tool/launch-intents") .join(intent_id) .join("intent.json") .is_file() ); } #[test] fn riscv_smoke_requires_each_explicit_mutation_flag_before_io() { let output = cli() .args([ "--format", "json", "riscv", "smoke", "--device", "unreached.invalid", "--expect-device-id", "ABC123", ]) .output() .unwrap(); assert_eq!(output.status.code(), Some(2)); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("--allow-ftp-create") ); } #[test] fn jsonl_progress_and_run_inspection_are_versioned() { let (root, manifest) = fixture(); let run = cli() .args(["--format", "jsonl", "test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake"]) .output() .unwrap(); assert!( run.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&run.stderr), String::from_utf8_lossy(&run.stdout) ); assert!(run.stderr.is_empty()); let lines = String::from_utf8(run.stdout).unwrap(); let values = lines .lines() .map(|line| serde_json::from_str::(line).unwrap()) .collect::>(); assert!(values.len() > 2); assert_eq!(values.first().unwrap()["record_kind"], "progress"); assert_eq!(values.last().unwrap()["record_kind"], "result"); assert!(values.iter().all(|value| value["schema_version"] == 1)); let run_id = values.last().unwrap()["data"]["runs"][0]["run_id"] .as_str() .unwrap(); for arguments in [ vec!["runs", "list", "--manifest", manifest.to_str().unwrap()], vec![ "runs", "show", run_id, "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "resolve", run_id, "$B000", "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "crash-report", run_id, "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "compare", run_id, run_id, "--manifest", manifest.to_str().unwrap(), ], ] { let output = cli() .args(["--format", "json"]) .args(arguments) .output() .unwrap(); assert!(output.status.success()); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); if value["command"] == "runs.resolve" { assert_eq!(value["data"]["state"], "unavailable"); assert_eq!(value["data"]["address"], 0xb000); assert_eq!(value["data"]["address_hex"], "$B000"); } else if value["command"] == "runs.compare" { assert_eq!(value["data"]["state"], "unavailable"); } else if value["command"] == "runs.crash_report" { assert_eq!(value["data"]["state"], "unavailable"); assert_eq!(value["data"]["unavailable_reason"], "no_crash_state"); } } let copied = root.path().join("copied-events.jsonl"); let evidence = cli() .args([ "--format", "json", "runs", "evidence", run_id, "events.jsonl", "--manifest", ]) .arg(&manifest) .arg("--output") .arg(&copied) .output() .unwrap(); assert!(evidence.status.success()); let value: Value = serde_json::from_slice(&evidence.stdout).unwrap(); assert_eq!(value["data"]["size"], fs::metadata(&copied).unwrap().len()); let no_overwrite = cli() .args([ "--format", "json", "runs", "evidence", run_id, "events.jsonl", "--manifest", ]) .arg(&manifest) .arg("--output") .arg(&copied) .output() .unwrap(); assert_eq!(no_overwrite.status.code(), Some(5)); assert!(no_overwrite.stderr.is_empty()); let value: Value = serde_json::from_slice(&no_overwrite.stdout).unwrap(); assert_eq!(value["error"]["code"], "evidence_failed"); let traversal = cli() .args([ "--format", "json", "runs", "evidence", run_id, "../events.jsonl", "--manifest", ]) .arg(&manifest) .output() .unwrap(); assert_eq!(traversal.status.code(), Some(5)); assert!(traversal.stderr.is_empty()); let value: Value = serde_json::from_slice(&traversal.stdout).unwrap(); assert_eq!(value["error"]["code"], "evidence_failed"); } #[test] fn strict_gate_flag_is_accepted_and_forwarded_without_io() { let (_root, manifest) = fixture(); // The probe surface advertises the shared flag without contacting anything. let probe_help = cli() .args(["probe", "ultimate", "--help"]) .output() .unwrap(); assert!(probe_help.status.success()); let help = String::from_utf8(probe_help.stdout).unwrap(); assert!(help.contains("--strict-gate"), "probe help: {help}"); // The plain fake run keeps its unchanged default. let ordinary = cli() .args(["test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake"]) .output() .unwrap(); assert!( ordinary.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&ordinary.stderr), String::from_utf8_lossy(&ordinary.stdout) ); // The forwarded flag reaches the core shape gate: one fake target is not a // supported strict shape, so the run fails before any target with the core's // own strict message rather than the ordinary pass above. let strict = cli() .args(["--format", "json", "test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake", "--strict-gate"]) .output() .unwrap(); assert!(!strict.status.success()); let value: Value = serde_json::from_slice(&strict.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("strict_gate"), "message: {value}" ); }