From 928ff0eabe9e78c16febd06da02091d801bb5395 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 30 Aug 2026 20:45:40 +0900 Subject: [PATCH] fix: isolate standalone target resolution --- crates/yoi/src/main.rs | 139 ++++++++++++++++++++++----- docs/design/standalone-agent-host.md | 2 +- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/crates/yoi/src/main.rs b/crates/yoi/src/main.rs index 48d19706..d40a082e 100644 --- a/crates/yoi/src/main.rs +++ b/crates/yoi/src/main.rs @@ -217,6 +217,24 @@ fn resolve_tui_target( selection: &TargetSelection, workspace_root: &Path, ) -> Result, ParseError> { + if selection.explicit_local { + return resolve_connection_aware_cli_connection( + connection_resolver, + command, + true, + None, + None, + ); + } + + if selection.backend_url.is_none() + && let Ok(target) = + resolve_connection_aware_cli_connection(connection_resolver, command, false, None, None) + && target.kind() == TargetKind::Standalone + { + return Ok(target); + } + let workspace_id = match selection.workspace_id.clone() { Some(workspace_id) => Some(workspace_id), None => resolve_workspace_id_from_root(workspace_root)?, @@ -1160,23 +1178,22 @@ fn read_client_default_connection() -> Result Result, ParseError> { + let path = client_global_config_path(); + read_client_config_from_global_path(path.as_deref()) +} + +fn read_client_config_from_global_path( + path: Option<&Path>, +) -> Result, ParseError> { + let Some(path) = path else { + return Ok(None); + }; + let Some(overlay) = read_client_config_overlay(path)? else { + return Ok(None); + }; let mut config = ClientConfigFile::default(); - let mut found = false; - - if let Some(path) = client_global_config_path() { - if let Some(overlay) = read_client_config_overlay(&path)? { - config.apply_overlay(overlay); - found = true; - } - } - - let cwd_path = client_cwd_config_path()?; - if let Some(overlay) = read_client_config_overlay(&cwd_path)? { - config.apply_overlay(overlay); - found = true; - } - - Ok(found.then_some(config)) + config.apply_overlay(overlay); + Ok(Some(config)) } fn read_client_config_overlay(path: &Path) -> Result, ParseError> { @@ -1194,14 +1211,10 @@ fn client_global_config_path() -> Option { manifest::paths::data_dir().map(|dir| dir.join("client").join("config.toml")) } -fn client_cwd_config_path() -> Result { - Ok(current_dir()?.join(".yoi").join("client.config.toml")) -} - fn client_config_location_message() -> String { match client_global_config_path() { - Some(path) => format!("{} or /.yoi/client.config.toml", path.display()), - None => "/client/config.toml or /.yoi/client.config.toml".to_string(), + Some(path) => path.display().to_string(), + None => "/client/config.toml".to_string(), } } @@ -2249,6 +2262,88 @@ backend = "shared" } } + #[test] + fn explicit_standalone_ignores_malformed_repository_workspace_identity() { + let repository = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(repository.path().join(".yoi")).unwrap(); + std::fs::write( + repository.path().join(".yoi/workspace.toml"), + "this is not valid toml = [", + ) + .unwrap(); + let resolver = FixedCliConnectionResolver { + backend_url: "http://fake-backend.example", + }; + + let target = resolve_tui_target( + &resolver, + CliCommand::DefaultTui, + &TargetSelection { + explicit_local: true, + ..TargetSelection::default() + }, + repository.path(), + ) + .unwrap(); + + assert_eq!(target.kind(), TargetKind::Standalone); + } + + #[test] + fn default_standalone_ignores_malformed_repository_workspace_identity() { + let repository = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(repository.path().join(".yoi")).unwrap(); + std::fs::write( + repository.path().join(".yoi/workspace.toml"), + "this is not valid toml = [", + ) + .unwrap(); + let resolver = FixedCliConnectionResolver { + backend_url: "http://fake-backend.example", + }; + + let target = resolve_tui_target( + &resolver, + CliCommand::DefaultTui, + &TargetSelection::default(), + repository.path(), + ) + .unwrap(); + + assert_eq!(target.kind(), TargetKind::Standalone); + } + + #[test] + fn client_config_reader_uses_only_the_supplied_global_path() { + let root = tempfile::tempdir().unwrap(); + let global = root.path().join("client/config.toml"); + std::fs::create_dir_all(global.parent().unwrap()).unwrap(); + std::fs::write( + &global, + "default_connection = \"local\"\n[backends.main]\nurl = \"http://backend.example\"\n", + ) + .unwrap(); + std::fs::create_dir_all(root.path().join("repository/.yoi")).unwrap(); + std::fs::write( + root.path().join("repository/.yoi/client.config.toml"), + "this is not valid toml = [", + ) + .unwrap(); + + let config = read_client_config_from_global_path(Some(&global)) + .unwrap() + .unwrap(); + + assert_eq!( + config.default_connection, + ClientDefaultConnection::Standalone + ); + assert_eq!( + config.backends["main"].url.as_deref(), + Some("http://backend.example") + ); + } + #[test] fn default_backend_target_inherits_workspace_identity_from_workspace_root() { let workspace = tempfile::tempdir().unwrap(); diff --git a/docs/design/standalone-agent-host.md b/docs/design/standalone-agent-host.md index 96fd646a..3eff3604 100644 --- a/docs/design/standalone-agent-host.md +++ b/docs/design/standalone-agent-host.md @@ -31,7 +31,7 @@ worker ## CLI / TUI routing -- `yoi` の connection-aware command は `TargetKind::Standalone | Backend` の二択で dispatch する。`--local` と client config の `default_connection = "local"` は Standalone を選ぶ入力であり、旧 LocalBackend を有効化しない。 +- `yoi` の connection-aware command は `TargetKind::Standalone | Backend` の二択で dispatch する。`--local` と client config の `default_connection = "local"` は Standalone を選ぶ入力であり、旧 LocalBackend を有効化しない。 Client config は repository `.yoi/client.config.toml` を読まず、repository `.yoi/workspace.toml` は Backend Workspace identity が必要な場合だけ参照する。 - Standalone の通常起動は `StandaloneHost`、restore は専用 `StandaloneStore` の session picker を使う。Workspace Worker list、PID、Unix socket、subprocess は探索しない。 - `workers`、Backend Worker restore、Workspace panel、Ticket、Objective は Backend authority を要求する。Standalone から repository-local filesystem backend へ fallback しない。 - `yoi worker` は Runtime や明示的な process-owned integration が使う direct Worker entrypoint として残るが、通常の `yoi` / TUI 起動経路からは呼び出さない。