33f5ad68bf
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m26s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Native shell tests (push) Successful in 10m54s
Project CI / Backend tests (push) Successful in 12m42s
Project CI / Frontend tests (push) Successful in 13m4s
Project CI / AI game creator shell web tests (push) Successful in 5m1s
Project CI / Repository checks (push) Successful in 12m48s
AGC 原有插件系统无法直接操作已打开的 Unity Editor。本变更增加内置 `agc-unity-editor`,在 Windows x64 / Unity Mono 上支持当前项目探测、连接与 C# 执行,不向 Unity 工程安装 UPM 桥接包。 ## 主要变更 - 固定复用 DotCraft.Unity 0.4.3 的 Attach 核心,提供自包含 .NET helper,保留上游许可证、来源及修改记录。 - GUI、Runtime、DirectProject 共用 Runner 执行服务;补齐项目身份、并发、总期限、回执确认与持久不确定状态阻断。 - 现有打开项目入口支持 Unity,按项目类型及开关暴露插件和 Agent 工具。 - Windows 构建准备 helper 并随包分发;插件 JS/Rust 测试接入现有 CI 组,Jenkins 增加 .NET 10 工具链预检。 ## 验证 - .NET helper 27 项测试、自包含发布及最小环境协议 smoke 通过。 - Unity 6000.3.7f1 实机验证通过:连接、C# 执行、编译错误修复、断连重连、Domain Reload 后重新握手;真实 Runner 的 ACK、并发拒绝和跨重启阻断通过。 - 宿主 Unity、PluginHost、Cocos、MCP、工具目录与引擎识别定向回归通过;前端类型检查、插件 JS/Rust、CI 配置、格式、编码和文档门禁通过。 Linux CI 不代替 Windows helper/实机验证;发行安装包 UI smoke、其它 Unity 版本和 Unity CoreCLR 未验证。Unity 演示工程中的场景和组件已撤销,不在此 PR 范围内。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/423
153 lines
5.8 KiB
Rust
153 lines
5.8 KiB
Rust
use super::*;
|
|
|
|
fn envelope(id: u64, result: Value) -> Vec<u8> {
|
|
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::<RpcParams>(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());
|
|
}
|