接入 Godot 编辑器插件与受控执行链路
新增 GDExtension 自动引导、GDScript 执行和实例隔离缓存 接入 AGC 插件开关、Runner、Agent 工具与权限审计 完善执行回执确认、不确定状态阻断及卸载恢复 补齐 Windows 分发资源、定向测试与实机验收文档
This commit is contained in:
@@ -509,7 +509,7 @@ fn send_external_agent_runner_request_with_protocol_and_id_and_timeouts(
|
||||
pub(super) fn external_agent_runner_client_read_timeout(method: &str) -> Duration {
|
||||
match method {
|
||||
"runtime.compact" => EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT,
|
||||
"unity.editor.rpc" => Duration::from_secs(80),
|
||||
"unity.editor.rpc" | "godot.editor.rpc" => Duration::from_secs(80),
|
||||
_ => EXTERNAL_AGENT_RUNNER_IO_TIMEOUT,
|
||||
}
|
||||
}
|
||||
@@ -1680,21 +1680,33 @@ pub(super) fn send_external_agent_runner_runtime_request_with_stable_identity(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Result<Value, String> {
|
||||
pub(crate) fn call_external_managed_editor(
|
||||
editor: crate::editor_adapters::ManagedEditor,
|
||||
method: &str,
|
||||
mut params: Value,
|
||||
) -> Result<Value, String> {
|
||||
let deadline = Instant::now() + Duration::from_secs(80);
|
||||
static EXECUTION_UNCERTAIN: std::sync::atomic::AtomicBool =
|
||||
static UNITY_UNCERTAIN: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
static GODOT_UNCERTAIN: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
let execution_uncertain = match editor {
|
||||
crate::editor_adapters::ManagedEditor::Unity => &UNITY_UNCERTAIN,
|
||||
crate::editor_adapters::ManagedEditor::Godot => &GODOT_UNCERTAIN,
|
||||
};
|
||||
let config_dir = external_agent_runner_config_dir().ok_or("外部 Agent Runner 尚未配置")?;
|
||||
let uncertain_result = || serde_json::json!({"ok":false,"status":"needs-reconciliation","retryAllowed":false,"dispatched":true,"error":{"code":"runner-receipt-unconfirmed","message":"Unity 执行回执未确认,核对后退出全部 AGC 和 Runner 再重新打开"}});
|
||||
if method == "execute" && EXECUTION_UNCERTAIN.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
let uncertain_result = || serde_json::json!({"ok":false,"status":"needs-reconciliation","retryAllowed":false,"dispatched":true,"error":{"code":"runner-receipt-unconfirmed","message":"编辑器 执行回执未确认,核对后退出全部 AGC 和 Runner 再重新打开"}});
|
||||
if method == "execute" && execution_uncertain.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Ok(uncertain_result());
|
||||
}
|
||||
if method == "execute"
|
||||
&& crate::editor_adapters::unity_uncertain_fence_path(&config_dir).exists()
|
||||
&& crate::editor_adapters::editor_uncertain_fence_path(editor, &config_dir).exists()
|
||||
{
|
||||
return Ok(uncertain_result());
|
||||
}
|
||||
let endpoint = if crate::editor_adapters::unity_execution_fence_path(&config_dir).exists() {
|
||||
let endpoint = if crate::editor_adapters::editor_execution_fence_path(editor, &config_dir)
|
||||
.exists()
|
||||
{
|
||||
// 在途 fence 可能只是正常并发;由活着的 owner 区分 busy 与 unknown。
|
||||
// 此分支绝不自动重启 Runner,以免丢失未确认执行的进程内状态。
|
||||
match read_external_agent_runner_endpoint(&external_agent_runner_endpoint_path(&config_dir))
|
||||
@@ -1707,7 +1719,7 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
let _configure = match external_agent_runner_configure_lock().try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(_) if method == "execute" => {
|
||||
return Ok(crate::editor_adapters::unity_not_dispatched(
|
||||
return Ok(crate::editor_adapters::editor_not_dispatched(
|
||||
"Runner 正在配置,请等待当前操作完成",
|
||||
))
|
||||
}
|
||||
@@ -1718,11 +1730,11 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining < Duration::from_secs(16) {
|
||||
return if method == "execute" {
|
||||
Ok(crate::editor_adapters::unity_not_dispatched(
|
||||
"Unity 调用启动预算已耗尽,未派发执行",
|
||||
Ok(crate::editor_adapters::editor_not_dispatched(
|
||||
"编辑器 调用启动预算已耗尽,未派发执行",
|
||||
))
|
||||
} else {
|
||||
Err("Unity 调用启动预算已耗尽".to_string())
|
||||
Err("编辑器 调用启动预算已耗尽".to_string())
|
||||
};
|
||||
}
|
||||
if let Some(params) = params.as_object_mut() {
|
||||
@@ -1732,7 +1744,7 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
.is_some_and(|timeout| (1..=60_000).contains(&timeout))
|
||||
}) {
|
||||
return if method == "execute" {
|
||||
Ok(crate::editor_adapters::unity_not_dispatched(
|
||||
Ok(crate::editor_adapters::editor_not_dispatched(
|
||||
"timeoutMs 必须在 1..=60000",
|
||||
))
|
||||
} else {
|
||||
@@ -1746,7 +1758,13 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
let bounded = requested.min(remaining.as_millis().saturating_sub(15_000) as u64);
|
||||
params.insert("timeoutMs".to_string(), serde_json::json!(bounded));
|
||||
}
|
||||
let request_id = random_identifier(b"agc-unity-editor-request")?;
|
||||
let request_id = random_identifier(b"agc-editor-request")?;
|
||||
// 所有可能失败的随机身份生成必须在真实执行派发前完成。
|
||||
let acknowledgement_id = random_identifier(b"agc-editor-ack")?;
|
||||
let persist_uncertain = || {
|
||||
execution_uncertain.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
let _ = crate::editor_adapters::mark_editor_execution_uncertain_at(editor, &config_dir);
|
||||
};
|
||||
let request_params = ExternalAgentRunnerRequestParams {
|
||||
editor_rpc: Some(
|
||||
serde_json::json!({"method":method,"params":params,"deadlineMs":unix_millis()+remaining.as_millis() as u64}),
|
||||
@@ -1758,7 +1776,7 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
&endpoint,
|
||||
EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id.clone(),
|
||||
"unity.editor.rpc",
|
||||
editor.rpc_method(),
|
||||
request_params,
|
||||
Duration::from_secs(2),
|
||||
remaining.saturating_sub(Duration::from_secs(7)),
|
||||
@@ -1767,17 +1785,16 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
match response {
|
||||
Ok(mut value) => {
|
||||
if method == "execute" {
|
||||
if !crate::editor_adapters::unity_execute_receipt_is_valid(&value) {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
if !crate::editor_adapters::editor_execute_receipt_is_valid(&value) {
|
||||
persist_uncertain();
|
||||
return Ok(uncertain_result());
|
||||
}
|
||||
if value["status"] == "needs-reconciliation" {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
persist_uncertain();
|
||||
return Ok(value);
|
||||
}
|
||||
let Some(ack_required) = value.get("ackRequired").and_then(Value::as_bool) else {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
let _ = crate::editor_adapters::mark_unity_execution_uncertain_at(&config_dir);
|
||||
persist_uncertain();
|
||||
return Ok(uncertain_result());
|
||||
};
|
||||
if let Some(object) = value.as_object_mut() {
|
||||
@@ -1787,15 +1804,15 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
return Ok(value);
|
||||
}
|
||||
if Instant::now() + Duration::from_secs(3) >= deadline {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
persist_uncertain();
|
||||
return Ok(uncertain_result());
|
||||
}
|
||||
let acknowledgement =
|
||||
send_external_agent_runner_request_with_protocol_and_id_and_timeouts(
|
||||
&endpoint,
|
||||
EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
random_identifier(b"agc-unity-ack")?,
|
||||
"unity.editor.ack",
|
||||
acknowledgement_id,
|
||||
editor.ack_method(),
|
||||
ExternalAgentRunnerRequestParams {
|
||||
editor_rpc: Some(serde_json::json!({"requestId":request_id})),
|
||||
..Default::default()
|
||||
@@ -1805,25 +1822,47 @@ pub(crate) fn call_external_unity_editor(method: &str, mut params: Value) -> Res
|
||||
Duration::from_millis(500),
|
||||
);
|
||||
if !acknowledgement.is_ok_and(|response| response["acknowledged"] == true) {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
let _ = crate::editor_adapters::mark_unity_execution_uncertain_at(&config_dir);
|
||||
persist_uncertain();
|
||||
return Ok(uncertain_result());
|
||||
}
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
Err(_) if method == "execute" => {
|
||||
EXECUTION_UNCERTAIN.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
persist_uncertain();
|
||||
Ok(uncertain_result())
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn disconnect_external_unity_editor() -> Result<(), String> {
|
||||
pub(crate) fn disconnect_external_managed_editor(
|
||||
editor: crate::editor_adapters::ManagedEditor,
|
||||
) -> Result<(), String> {
|
||||
disconnect_external_managed_editor_project(editor, None)
|
||||
}
|
||||
|
||||
pub(crate) fn disconnect_external_managed_editor_project(
|
||||
editor: crate::editor_adapters::ManagedEditor,
|
||||
project: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let Some(config_dir) = external_agent_runner_config_dir() else {
|
||||
return Ok(());
|
||||
return if editor == crate::editor_adapters::ManagedEditor::Godot
|
||||
&& project
|
||||
.map(crate::editor_adapters::godot_project_cleanup_required)
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Err("Godot 清理宿主尚未初始化,无法确认旧桥已卸载".into())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
};
|
||||
if editor == crate::editor_adapters::ManagedEditor::Godot {
|
||||
return disconnect_external_godot_projects(&config_dir, project, |params| {
|
||||
call_external_managed_editor(editor, "disconnect", params)
|
||||
});
|
||||
}
|
||||
let path = external_agent_runner_endpoint_path(&config_dir);
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
@@ -1831,7 +1870,7 @@ pub(crate) fn disconnect_external_unity_editor() -> Result<(), String> {
|
||||
let endpoint = read_external_agent_runner_endpoint(&path)?;
|
||||
send_external_agent_runner_request(
|
||||
&endpoint,
|
||||
"unity.editor.rpc",
|
||||
editor.rpc_method(),
|
||||
ExternalAgentRunnerRequestParams {
|
||||
editor_rpc: Some(serde_json::json!({"method":"disconnect","params":{}})),
|
||||
..Default::default()
|
||||
@@ -1840,17 +1879,72 @@ pub(crate) fn disconnect_external_unity_editor() -> Result<(), String> {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn mark_external_unity_editor_uncertain() -> Result<(), String> {
|
||||
fn disconnect_external_godot_projects(
|
||||
config: &Path,
|
||||
explicit: Option<&Path>,
|
||||
mut cleanup: impl FnMut(Value) -> Result<Value, String>,
|
||||
) -> Result<(), String> {
|
||||
// endpoint 丢失不等于编辑器桥消失;用持久授权根启动原 owner 的清理流程。
|
||||
for project in crate::editor_adapters::godot_cleanup_projects_at(config, explicit)? {
|
||||
let result = cleanup(serde_json::json!({"projectPath":project}))?;
|
||||
if result["adapter"] != "godot-editor"
|
||||
|| result["connected"] != false
|
||||
|| result.get("error").is_some()
|
||||
|| result.get("accepted").is_some()
|
||||
|| result["status"] == "needs-reconciliation"
|
||||
{
|
||||
return Err("Godot 原生桥尚未确认卸载".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod managed_cleanup_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn godot_cleanup_recovers_authorized_projects_without_runner_endpoint() {
|
||||
let config = tempfile::tempdir().unwrap();
|
||||
let project = tempfile::tempdir().unwrap();
|
||||
let expected = project.path().canonicalize().unwrap();
|
||||
fs::write(
|
||||
config.path().join("godot-editor-authorized-projects.json"),
|
||||
serde_json::json!({
|
||||
"schemaVersion":"agc.godot.authorized-projects.v1", "projects":[expected]
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!external_agent_runner_endpoint_path(config.path()).exists());
|
||||
let mut called = false;
|
||||
let result = disconnect_external_godot_projects(config.path(), None, |params| {
|
||||
called = true;
|
||||
assert_eq!(params["projectPath"], serde_json::json!(expected));
|
||||
Ok(serde_json::json!({"accepted":true,"status":"shutting-down"}))
|
||||
});
|
||||
assert!(called);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
crate::editor_adapters::godot_authorized_projects_at(config.path()).unwrap(),
|
||||
vec![expected]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_external_editor_uncertain(
|
||||
editor: crate::editor_adapters::ManagedEditor,
|
||||
) -> Result<(), String> {
|
||||
let config_dir = external_agent_runner_config_dir().ok_or("外部 Agent Runner 尚未配置")?;
|
||||
// 先保存 GUI 与 Runner 共享的单向 fence;网络丢失也不能解锁。
|
||||
crate::editor_adapters::mark_unity_execution_uncertain_at(&config_dir)?;
|
||||
crate::editor_adapters::mark_editor_execution_uncertain_at(editor, &config_dir)?;
|
||||
let endpoint =
|
||||
read_external_agent_runner_endpoint(&external_agent_runner_endpoint_path(&config_dir))?;
|
||||
send_external_agent_runner_request_with_protocol_and_id_and_timeouts(
|
||||
&endpoint,
|
||||
EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
random_identifier(b"agc-unity-mark-uncertain")?,
|
||||
"unity.editor.mark_uncertain",
|
||||
random_identifier(b"agc-editor-mark-uncertain")?,
|
||||
editor.mark_method(),
|
||||
ExternalAgentRunnerRequestParams::default(),
|
||||
Duration::from_millis(500),
|
||||
Duration::from_secs(1),
|
||||
|
||||
@@ -236,7 +236,9 @@ pub(super) fn validate_external_agent_runner_gui_owner_claim_current(
|
||||
}
|
||||
|
||||
fn external_agent_runner_method_requires_current_gui_owner_claim(method: &str) -> bool {
|
||||
method.starts_with("runtime.") || method.starts_with("unity.editor.")
|
||||
method.starts_with("runtime.")
|
||||
|| method.starts_with("unity.editor.")
|
||||
|| method.starts_with("godot.editor.")
|
||||
}
|
||||
|
||||
pub(super) fn external_agent_runner_request_session_id(
|
||||
@@ -1031,8 +1033,10 @@ pub(super) fn handle_external_agent_runner_request(
|
||||
}
|
||||
|
||||
match request.method.as_str() {
|
||||
// 编辑器使用自身的有界并发门闩;不能持有 Runtime 全局写请求缓存锁等待 Unity。
|
||||
"unity.editor.rpc" => {
|
||||
// 编辑器使用自身的有界并发门闩;不能持有 Runtime 全局写请求缓存锁等待 编辑器。
|
||||
"unity.editor.rpc" | "godot.editor.rpc" => {
|
||||
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
|
||||
.expect("matched editor RPC");
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct EditorCall {
|
||||
@@ -1048,14 +1052,15 @@ pub(super) fn handle_external_agent_runner_request(
|
||||
let call: EditorCall = serde_json::from_value(
|
||||
request.params.editor_rpc.clone().ok_or("缺少 editorRpc")?,
|
||||
)
|
||||
.map_err(|_| "Unity RPC 参数无效".to_string())?;
|
||||
.map_err(|_| "编辑器 RPC 参数无效".to_string())?;
|
||||
if call
|
||||
.deadline_ms
|
||||
.is_some_and(|deadline| deadline <= unix_millis())
|
||||
{
|
||||
return Err("Unity RPC 派发期限已过,未发送执行".to_string());
|
||||
return Err("编辑器 RPC 派发期限已过,未发送执行".to_string());
|
||||
}
|
||||
crate::editor_adapters::unity_editor_rpc_owned(
|
||||
crate::editor_adapters::managed_editor_rpc_owned(
|
||||
editor,
|
||||
&call.method,
|
||||
call.params,
|
||||
Some(&request.request_id),
|
||||
@@ -1084,25 +1089,27 @@ pub(super) fn handle_external_agent_runner_request(
|
||||
.and_then(|value| value["method"].as_str())
|
||||
== Some("execute") =>
|
||||
{
|
||||
let mut value = crate::editor_adapters::unity_not_dispatched(&error);
|
||||
let mut value = crate::editor_adapters::editor_not_dispatched(&error);
|
||||
value["ackRequired"] = json!(false);
|
||||
ExternalAgentRunnerResponse::success(&request.request_id, value)
|
||||
}
|
||||
Err(error) => ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"unity-editor-failed",
|
||||
"editor-rpc-failed",
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
"unity.editor.ack" => {
|
||||
"unity.editor.ack" | "godot.editor.ack" => {
|
||||
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
|
||||
.expect("matched editor ACK");
|
||||
let result = request
|
||||
.params
|
||||
.editor_rpc
|
||||
.as_ref()
|
||||
.and_then(|value| value["requestId"].as_str())
|
||||
.ok_or_else(|| "缺少 Unity 回执身份".to_string())
|
||||
.and_then(crate::editor_adapters::acknowledge_unity_editor_delivery);
|
||||
.ok_or_else(|| "缺少 编辑器 回执身份".to_string())
|
||||
.and_then(|id| crate::editor_adapters::acknowledge_editor_delivery(editor, id));
|
||||
match result {
|
||||
Ok(()) => ExternalAgentRunnerResponse::success(
|
||||
&request.request_id,
|
||||
@@ -1110,20 +1117,22 @@ pub(super) fn handle_external_agent_runner_request(
|
||||
),
|
||||
Err(error) => ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"unity-ack-failed",
|
||||
"editor-ack-failed",
|
||||
error,
|
||||
),
|
||||
}
|
||||
}
|
||||
"unity.editor.mark_uncertain" => {
|
||||
match crate::editor_adapters::mark_unity_execution_uncertain() {
|
||||
"unity.editor.mark_uncertain" | "godot.editor.mark_uncertain" => {
|
||||
let editor = crate::editor_adapters::ManagedEditor::from_rpc_method(&request.method)
|
||||
.expect("matched editor uncertain RPC");
|
||||
match crate::editor_adapters::mark_editor_execution_uncertain(editor) {
|
||||
Ok(()) => ExternalAgentRunnerResponse::success(
|
||||
&request.request_id,
|
||||
json!({"status":"needs-reconciliation"}),
|
||||
),
|
||||
Err(error) => ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"unity-mark-failed",
|
||||
"editor-mark-failed",
|
||||
error,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1032,7 +1032,7 @@ pub(crate) fn acquire_external_agent_runner_gui_participant_lock(
|
||||
"Agent Runner 单实例锁",
|
||||
)?;
|
||||
if runner.is_some() {
|
||||
crate::editor_adapters::reset_unity_execution_fence_for_fresh_gui(config_dir)?;
|
||||
crate::editor_adapters::reset_editor_execution_fences_for_fresh_gui(config_dir)?;
|
||||
}
|
||||
drop(participant);
|
||||
runner
|
||||
|
||||
@@ -214,6 +214,7 @@ pub(crate) fn run_external_agent_runner_server(
|
||||
EXTERNAL_AGENT_RUNNER_SERVER_PROCESS.store(true, Ordering::Release);
|
||||
crate::set_game_creator_runtime_config_dir(config_dir.clone());
|
||||
crate::editor_adapters::configure_unity_helper_for_runtime()?;
|
||||
crate::editor_adapters::configure_godot_payload_for_runtime(&config_dir)?;
|
||||
set_external_agent_runner_config_dir(config_dir.clone());
|
||||
|
||||
let boot_id = random_identifier(b"genarrative-agent-runner-boot-id")?;
|
||||
|
||||
@@ -21,47 +21,53 @@ use crate::{
|
||||
static TEST_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn unity_pending_execution_survives_new_window_and_runner_restart_until_full_gui_restart() {
|
||||
let directory = unique_test_directory();
|
||||
let config = private_runner_test_config_dir(&directory);
|
||||
let first = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
let fence = crate::editor_adapters::unity_execution_fence_path(&config);
|
||||
fs::write(&fence, "unknown-request").unwrap();
|
||||
crate::editor_adapters::mark_unity_execution_uncertain_at(&config).unwrap();
|
||||
let uncertain_fence = crate::editor_adapters::unity_uncertain_fence_path(&config);
|
||||
let second = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(
|
||||
fence.exists(),
|
||||
"new window must not clear pending execution"
|
||||
);
|
||||
drop(second);
|
||||
let runner = acquire_external_agent_runner_instance_lock(
|
||||
&external_agent_runner_lock_path(&config),
|
||||
"unity-test-boot",
|
||||
)
|
||||
.unwrap();
|
||||
drop(first);
|
||||
let while_runner_alive = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(fence.exists(), "running owner prevents recovery");
|
||||
drop(while_runner_alive);
|
||||
drop(runner);
|
||||
let restarted_runner = acquire_external_agent_runner_instance_lock(
|
||||
&external_agent_runner_lock_path(&config),
|
||||
"unity-test-boot-2",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
fence.exists(),
|
||||
"automatic Runner restart must not clear pending execution"
|
||||
);
|
||||
assert!(uncertain_fence.exists());
|
||||
drop(restarted_runner);
|
||||
let _fresh = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(
|
||||
!fence.exists(),
|
||||
"all GUI and Runner exited: a fresh GUI can recover"
|
||||
);
|
||||
assert!(!uncertain_fence.exists());
|
||||
fn editor_pending_execution_survives_new_window_and_runner_restart_until_full_gui_restart() {
|
||||
for editor in [
|
||||
crate::editor_adapters::ManagedEditor::Unity,
|
||||
crate::editor_adapters::ManagedEditor::Godot,
|
||||
] {
|
||||
let directory = unique_test_directory();
|
||||
let config = private_runner_test_config_dir(&directory);
|
||||
let first = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
let fence = crate::editor_adapters::editor_execution_fence_path(editor, &config);
|
||||
fs::write(&fence, "unknown-request").unwrap();
|
||||
crate::editor_adapters::mark_editor_execution_uncertain_at(editor, &config).unwrap();
|
||||
let uncertain_fence = crate::editor_adapters::editor_uncertain_fence_path(editor, &config);
|
||||
let second = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(
|
||||
fence.exists(),
|
||||
"new window must not clear pending execution"
|
||||
);
|
||||
drop(second);
|
||||
let runner = acquire_external_agent_runner_instance_lock(
|
||||
&external_agent_runner_lock_path(&config),
|
||||
"editor-test-boot",
|
||||
)
|
||||
.unwrap();
|
||||
drop(first);
|
||||
let while_runner_alive =
|
||||
acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(fence.exists(), "running owner prevents recovery");
|
||||
drop(while_runner_alive);
|
||||
drop(runner);
|
||||
let restarted_runner = acquire_external_agent_runner_instance_lock(
|
||||
&external_agent_runner_lock_path(&config),
|
||||
"editor-test-boot-2",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
fence.exists(),
|
||||
"automatic Runner restart must not clear pending execution"
|
||||
);
|
||||
assert!(uncertain_fence.exists());
|
||||
drop(restarted_runner);
|
||||
let _fresh = acquire_external_agent_runner_gui_participant_lock(&config).unwrap();
|
||||
assert!(
|
||||
!fence.exists(),
|
||||
"all GUI and Runner exited: a fresh GUI can recover"
|
||||
);
|
||||
assert!(!uncertain_fence.exists());
|
||||
}
|
||||
}
|
||||
|
||||
struct TestDirectoryGuard(PathBuf);
|
||||
|
||||
Reference in New Issue
Block a user