Files
Genarrative/plugins/agc-unity-editor/native/unity-editor-bridge/tests/protocol.rs
T
kdletters 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
接入 DotCraft Unity 编辑器插件与受控执行链路 (#423)
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
2026-09-19 12:29:27 +08:00

133 lines
4.8 KiB
Rust

use editor_adapter_api::EditorAdapter;
use serde_json::json;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use unity_editor_bridge::{
configure_helper_candidates, disconnect_unity_editor, execute_unity_editor_code_for_project,
UnityEditorAdapter,
};
const MAX_MESSAGE_BYTES: usize = unity_editor_bridge::MAX_MESSAGE_BYTES;
#[path = "../src/transport.rs"]
mod transport;
fn fixture() -> (tempfile::TempDir, PathBuf) {
let directory = tempfile::tempdir().unwrap();
let executable = directory
.path()
.join("dotnet/publish/win-x64/Agc.Unity.Attach.exe");
std::fs::create_dir_all(executable.parent().unwrap()).unwrap();
std::fs::copy(
env!("CARGO_BIN_EXE_unity-bridge-protocol-fixture"),
&executable,
)
.unwrap();
(directory, executable)
}
fn project() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
for path in ["Assets", "Packages", "ProjectSettings"] {
std::fs::create_dir(directory.path().join(path)).unwrap();
}
std::fs::write(
directory.path().join("ProjectSettings/ProjectVersion.txt"),
"m_EditorVersion: 6000.0.0f1\n",
)
.unwrap();
directory
}
#[test]
#[cfg(all(windows, target_arch = "x86_64"))]
fn shared_service_executes_serially_and_retains_uncertain_state_across_instances() {
let (_fixture, executable) = fixture();
let project = project();
let project_path = project.path().to_string_lossy().into_owned();
configure_helper_candidates(vec![executable.clone()]).unwrap();
std::env::set_var("AGC_TEST_PROVIDER_SECRET", "must-not-reach-helper");
let first = execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap();
std::env::remove_var("AGC_TEST_PROVIDER_SECRET");
assert_eq!(first["status"], "completed");
assert_eq!(first["result"]["secretInherited"], false);
for code in ["known_failure", "not_dispatched"] {
assert_eq!(
execute_unity_editor_code_for_project(&project_path, code, 1000).unwrap()["status"],
"failed"
);
}
let adapter = UnityEditorAdapter::new(vec![executable.clone()]);
assert_eq!(
adapter
.rpc(
"editor.execute",
json!({"projectPath":project_path,"code":"success","timeoutMs":1000})
)
.unwrap()["status"],
"completed"
);
let path = project_path.clone();
let executing =
std::thread::spawn(move || execute_unity_editor_code_for_project(&path, "delay", 1000));
std::thread::sleep(Duration::from_millis(50));
let start = Instant::now();
assert!(
execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap()["error"]
["message"]
.as_str()
.unwrap()
.contains("正在执行")
);
assert!(start.elapsed() < Duration::from_millis(150));
disconnect_unity_editor();
assert_eq!(executing.join().unwrap().unwrap()["status"], "completed");
let next = execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap();
assert_ne!(first["result"]["fixturePid"], next["result"]["fixturePid"]);
let invalid = execute_unity_editor_code_for_project(&project_path, "wrong_id", 1000).unwrap();
assert_eq!(invalid["status"], "needs-reconciliation");
disconnect_unity_editor();
configure_helper_candidates(vec![executable.clone()]).unwrap();
let recreated = UnityEditorAdapter::new(vec![executable]);
assert_eq!(
recreated
.rpc(
"execute",
json!({"projectPath":project_path,"code":"success"})
)
.unwrap()["status"],
"needs-reconciliation"
);
}
#[test]
fn helper_timeout_eof_oversized_frame_and_blocked_write_are_bounded() {
let (_fixture, executable) = fixture();
for code in ["eof", "overflow", "no_response"] {
let mut process = transport::HelperProcess::spawn(&executable).unwrap();
let result = process.exchange(
&json!({"id":1,"method":"execute","params":{"code":code}}),
Instant::now() + Duration::from_millis(300),
);
assert!(result.is_err());
let start = Instant::now();
drop(process);
assert!(start.elapsed() < Duration::from_secs(3));
}
let mut process = transport::HelperProcess::spawn(&executable).unwrap();
process
.exchange(
&json!({"id":1,"method":"execute","params":{"code":"stop_reading"}}),
Instant::now() + Duration::from_secs(2),
)
.unwrap();
let start = Instant::now();
assert!(process
.exchange(
&json!({"payload":"x".repeat(1024*1024)}),
Instant::now() + Duration::from_millis(100)
)
.is_err());
drop(process);
assert!(start.elapsed() < Duration::from_secs(3));
}