use super::*; fn envelope(id: u64, result: Value) -> Vec { serde_json::to_vec( &json!({"jsonrpc":"2.0","protocol":PROTOCOL_VERSION,"id":id,"result":result}), ) .unwrap() } #[test] fn only_complete_execution_receipts_are_trusted() { let complete = json!({"ok":true,"status":"completed","retryAllowed":false,"dispatched":true,"result":null}); assert!(parse_response(&envelope(1, complete.clone()), 1, "execute", "").is_ok()); assert!(parse_response(&envelope(2, complete.clone()), 1, "execute", "").is_err()); for (key, replacement) in [ ("status", json!("lost")), ("retryAllowed", json!(true)), ("dispatched", json!(false)), ("ok", json!(false)), ] { let mut bad = complete.clone(); bad[key] = replacement; assert!(parse_response(&envelope(1, bad), 1, "execute", "").is_err()); } assert!(parse_response(b"not-json", 1, "execute", "").is_err()); } #[test] fn known_runtime_and_pre_dispatch_failure_do_not_latch() { let service = UnityEditorService::default(); for dispatched in [false, true] { let failed = json!({"ok":false,"status":"failed","retryAllowed":false,"dispatched":dispatched, "error":{"code":"compile-failed","message":"known failure"}}); let receipt = parse_response(&envelope(1, failed), 1, "execute", ""); assert_eq!( service.finish_response("execute", receipt).unwrap()["status"], "failed" ); assert!(!service.uncertain.load(Ordering::SeqCst)); } } #[test] fn malformed_top_level_errors_eof_and_uncertain_status_latch() { for response in [ Err("EOF".into()), parse_response(br#"{"jsonrpc":"2.0","protocol":"agc.unity.attach.v1","id":1,"error":{"code":"x","message":"x"}}"#,1,"execute",""), Ok(reconciliation("unknown")), ] { let service = UnityEditorService::default(); assert_eq!(service.finish_response("execute",response).unwrap()["status"],"needs-reconciliation"); service.disconnect(); service.configure(Vec::new()).unwrap(); assert!(service.uncertain.load(Ordering::SeqCst)); } } #[test] fn fixed_helper_candidates_reject_arbitrary_payloads() { let root = tempfile::tempdir().unwrap(); assert!(validate_candidate(&root.path().join("any.exe")).is_err()); assert!(validate_candidate(Path::new("dotnet/publish/win-x64/Agc.Unity.Attach.exe")).is_err()); assert!(validate_candidate( &root .path() .join("dotnet/publish/win-x64/Agc.Unity.Attach.exe") ) .is_ok()); let value = json!({"projectPath":"x","helperPath":"x"}); assert!(serde_json::from_value::(value).is_err()); } #[test] fn code_limit_counts_utf8_bytes_and_refuses_nul() { assert!(validate_code("").is_err()); assert!(validate_code(" \n ").is_err()); assert!(validate_code("x\0y").is_err()); assert!(validate_code(&"δΈ­".repeat(MAX_EXECUTE_CODE_BYTES / 3 + 1)).is_err()); assert!(validate_code("return 42;").is_ok()); } #[test] fn disconnect_does_not_wait_for_inflight_request() { let service = UnityEditorService::default(); let _guard = service.state.lock().unwrap(); service.disconnect(); assert_eq!(service.epoch.load(Ordering::SeqCst), 1); } #[test] fn repeated_configuration_does_not_invalidate_connection() { let service = UnityEditorService::default(); let root = tempfile::tempdir().unwrap(); let candidates = vec![root .path() .join("dotnet/publish/win-x64/Agc.Unity.Attach.exe")]; service.configure(candidates.clone()).unwrap(); let epoch = service.epoch.load(Ordering::SeqCst); service.configure(candidates).unwrap(); assert_eq!(service.epoch.load(Ordering::SeqCst), epoch); } #[test] fn connection_receipts_require_project_process_and_generation_identity() { let project = tempfile::tempdir().unwrap(); let other = tempfile::tempdir().unwrap(); let expected = project.path().to_str().unwrap(); let result = json!({"adapter":"unity-editor","connected":true,"pid":42, "projectPath":expected,"version":"6000.0.0f1", "startedUtc":"2026-09-18T00:00:00Z","generation":"domain-1"}); assert!(parse_response(&envelope(1, result.clone()), 1, "connect", expected).is_ok()); for (key, replacement) in [ ("projectPath", json!(other.path())), ("pid", Value::Null), ("generation", Value::Null), ("startedUtc", json!("")), ("connected", json!(false)), ] { let mut invalid = result.clone(); invalid[key] = replacement; assert!(parse_response(&envelope(1, invalid), 1, "connect", expected).is_err()); } } #[test] fn invalid_connect_invalidates_previous_session_before_parameter_validation() { let service = UnityEditorService::default(); let epoch = service.epoch.load(Ordering::SeqCst); assert!(service.call("connect", RpcParams::default()).is_err()); assert!(service.epoch.load(Ordering::SeqCst) > epoch); assert!(!service.uncertain.load(Ordering::SeqCst)); } #[test] fn pre_dispatch_errors_have_explicit_non_execution_receipts() { let service = UnityEditorService::default(); let response = execution_result(service.call("execute", RpcParams::default())).unwrap(); assert_eq!(response["status"], "failed"); assert_eq!(response["dispatched"], false); assert_eq!(response["retryAllowed"], false); assert!(!service.uncertain.load(Ordering::SeqCst)); } #[test] fn project_marker_parents_must_be_ordinary_directories() { let project = tempfile::tempdir().unwrap(); for directory in ["Assets", "Packages"] { fs::create_dir(project.path().join(directory)).unwrap(); } fs::write(project.path().join("ProjectSettings"), "not a directory").unwrap(); assert!(normalize_project(project.path().to_str().unwrap()).is_err()); }