diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner/endpoint.rs b/apps/ai-game-creator-shell/src-tauri/src/runner/endpoint.rs index b4c91e3fa..ccfe7b7af 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner/endpoint.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner/endpoint.rs @@ -810,11 +810,13 @@ pub(super) fn try_open_external_agent_runner_lock( .parent() .ok_or_else(|| format!("{label} 缺少 AppData 父目录:{}", path.display()))?; let private_parent = crate::inspect_game_creator_runtime_config_dir(parent)?; - let expected_path = private_parent.join(EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME); - if path != expected_path { + let runner_lock_path = private_parent.join(EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME); + let gui_owner_lock_path = private_parent.join(EXTERNAL_AGENT_RUNNER_GUI_OWNER_LOCK_FILE_NAME); + if path != runner_lock_path && path != gui_owner_lock_path { return Err(format!( - "{label} 必须位于已验证的私有 AppData 固定路径:{}", - expected_path.display() + "{label} 必须位于已验证的私有 AppData 固定锁路径:{} 或 {}", + runner_lock_path.display(), + gui_owner_lock_path.display() )); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs index e5b29e3f3..be0931424 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs @@ -33,6 +33,11 @@ fn unique_test_directory() -> TestDirectoryGuard { TestDirectoryGuard(path) } +fn private_runner_test_config_dir(directory: &TestDirectoryGuard) -> PathBuf { + crate::prepare_game_creator_runtime_config_dir(&directory.0.join("appdata")) + .expect("prepare private runner AppData") +} + fn acquire_project_owner_after_release( root: &Path, boot_id: &str, @@ -273,21 +278,23 @@ fn endpoint_reuse_requires_current_protocol_and_executable_identity() { #[test] fn idle_runner_shutdown_treats_missing_endpoint_as_already_stopped() { let directory = unique_test_directory(); - assert!(shutdown_external_agent_runner_if_idle_at(&directory.0) + let config_dir = private_runner_test_config_dir(&directory); + assert!(shutdown_external_agent_runner_if_idle_at(&config_dir) .expect("missing endpoint should already be stopped")); } #[test] fn gui_runner_shutdown_rejects_missing_endpoint_while_runner_lock_is_held() { let directory = unique_test_directory(); + let config_dir = private_runner_test_config_dir(&directory); let _lock = acquire_external_agent_runner_instance_lock( - &external_agent_runner_lock_path(&directory.0), + &external_agent_runner_lock_path(&config_dir), "gui-shutdown-held-lock", ) .expect("hold runner lock without endpoint"); let started = Instant::now(); - let error = shutdown_external_agent_runner_at(&directory.0) + let error = shutdown_external_agent_runner_at(&config_dir) .expect_err("held Runner lock means missing endpoint is not proof of shutdown"); assert!(error.contains("实例锁仍被占用")); @@ -300,6 +307,7 @@ fn gui_runner_shutdown_rejects_missing_endpoint_while_runner_lock_is_held() { #[test] fn gui_runner_shutdown_has_a_short_hard_timeout_for_an_unresponsive_endpoint() { let directory = unique_test_directory(); + let config_dir = private_runner_test_config_dir(&directory); let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)) .expect("bind unresponsive runner fixture"); let port = listener.local_addr().expect("fixture address").port(); @@ -309,7 +317,7 @@ fn gui_runner_shutdown_has_a_short_hard_timeout_for_an_unresponsive_endpoint() { port, ); write_external_agent_runner_endpoint_atomic( - &external_agent_runner_endpoint_path(&directory.0), + &external_agent_runner_endpoint_path(&config_dir), &endpoint, ) .expect("write unresponsive endpoint"); @@ -319,7 +327,7 @@ fn gui_runner_shutdown_has_a_short_hard_timeout_for_an_unresponsive_endpoint() { }); let started = Instant::now(); - let error = shutdown_external_agent_runner_at(&directory.0) + let error = shutdown_external_agent_runner_at(&config_dir) .expect_err("unresponsive runner must hit the GUI shutdown deadline"); assert!(error.contains("读取 Agent Runner 响应失败")); @@ -461,27 +469,29 @@ fn runner_endpoint_rejects_hard_links() { #[test] fn gui_owner_lock_allows_only_one_frontend_process_per_appdata() { let directory = unique_test_directory(); + let config_dir = private_runner_test_config_dir(&directory); let first = - acquire_external_agent_runner_gui_owner_lock(&directory.0).expect("first GUI owns AppData"); - let error = acquire_external_agent_runner_gui_owner_lock(&directory.0) + acquire_external_agent_runner_gui_owner_lock(&config_dir).expect("first GUI owns AppData"); + let error = acquire_external_agent_runner_gui_owner_lock(&config_dir) .expect_err("second GUI must not share the same Runner owner"); assert!(error.contains("其他进程运行")); drop(first); - acquire_external_agent_runner_gui_owner_lock(&directory.0) + acquire_external_agent_runner_gui_owner_lock(&config_dir) .expect("GUI owner lock is recoverable after the first frontend exits"); } #[test] fn attached_gui_owner_loss_forces_runner_shutdown() { let directory = unique_test_directory(); + let config_dir = private_runner_test_config_dir(&directory); let token = "gui-owner-monitor-token-gui-owner-monitor-token"; let state = ExternalAgentRunnerServerState::new( - directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), + config_dir.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), test_endpoint(token, "gui-owner-monitor-boot", 31319), ); let owner = - acquire_external_agent_runner_gui_owner_lock(&directory.0).expect("acquire GUI owner lock"); + acquire_external_agent_runner_gui_owner_lock(&config_dir).expect("acquire GUI owner lock"); let attached = handle_external_agent_runner_request( ExternalAgentRunnerRequest { protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, @@ -509,9 +519,10 @@ fn attached_gui_owner_loss_forces_runner_shutdown() { #[test] fn gui_owner_attach_rejects_missing_owner_lock() { let directory = unique_test_directory(); + let config_dir = private_runner_test_config_dir(&directory); let token = "gui-owner-missing-token-gui-owner-missing-token"; let state = ExternalAgentRunnerServerState::new( - directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), + config_dir.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), test_endpoint(token, "gui-owner-missing-boot", 31320), ); let response = handle_external_agent_runner_request( @@ -1684,6 +1695,21 @@ fn active_runner_lock_is_not_repaired_or_truncated() { assert_eq!(diagnostic["bootId"], "first-active-boot"); } +#[cfg(windows)] +#[test] +fn windows_runner_lock_rejects_unrecognized_fixed_name() { + let directory = unique_test_directory(); + let config_dir = crate::prepare_game_creator_runtime_config_dir(&directory.0.join("appdata")) + .expect("prepare private runner AppData"); + let unexpected_path = config_dir.join("unexpected-runner.lock"); + + let error = try_open_external_agent_runner_lock(&unexpected_path, "unexpected Runner lock") + .expect_err("only the instance and GUI owner lock names are allowed"); + + assert!(error.contains("固定锁路径")); + assert!(!unexpected_path.exists()); +} + #[cfg(windows)] #[test] fn windows_stale_runner_lock_is_reowned_for_token_user() {