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
133 lines
4.8 KiB
Rust
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));
|
|
}
|