fix: preserve standalone scope conflict details

This commit is contained in:
2026-08-31 19:28:09 +09:00
parent 95a81faf63
commit d7cdcde443
2 changed files with 81 additions and 1 deletions
+22 -1
View File
@@ -4,6 +4,7 @@ use std::time::Duration;
use agen::llm_client::client::LlmClient; use agen::llm_client::client::LlmClient;
use client::Client; use client::Client;
use client::transport::in_process::{Peer as InProcessPeer, Socket as InProcessSocket}; use client::transport::in_process::{Peer as InProcessPeer, Socket as InProcessSocket};
use manifest::ScopeRule;
use protocol::stream::{decode_method, encode_event}; use protocol::stream::{decode_method, encode_event};
use protocol::{Event, Method, WorkerId}; use protocol::{Event, Method, WorkerId};
use session_store::{ use session_store::{
@@ -18,6 +19,7 @@ use worker::ipc::protocol_session::{
WorkerProtocolSessionStreams, dispatch_worker_protocol_method, live_log_entry_event, WorkerProtocolSessionStreams, dispatch_worker_protocol_method, live_log_entry_event,
subscribe_worker_protocol_session, subscribe_worker_protocol_session,
}; };
use worker::runtime::worker_allocation::ScopeLockError;
use worker::{BootstrappedWorker, WorkerError, WorkerFilesystemAuthority, WorkerWorkspaceContext}; use worker::{BootstrappedWorker, WorkerError, WorkerFilesystemAuthority, WorkerWorkspaceContext};
use crate::launch::ResolvedStandaloneLaunch; use crate::launch::ResolvedStandaloneLaunch;
@@ -43,7 +45,7 @@ pub struct StandaloneHost {
lease: Option<StandaloneWorkerLease>, lease: Option<StandaloneWorkerLease>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] #[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum StandaloneStartupError { pub enum StandaloneStartupError {
#[error("the standalone state store could not be opened or validated")] #[error("the standalone state store could not be opened or validated")]
StateStore, StateStore,
@@ -53,6 +55,16 @@ pub enum StandaloneStartupError {
LeaseLivenessUnknown, LeaseLivenessUnknown,
#[error("the standalone Worker working directory is unavailable or changed")] #[error("the standalone Worker working directory is unavailable or changed")]
WorkingDirectoryUnavailable, WorkingDirectoryUnavailable,
#[error(
"requested scope `{}` conflicts with worker allocation `{competitor}` rule `{}`",
requested_rule.target.display(),
competitor_rule.target.display()
)]
ScopeConflict {
competitor: String,
requested_rule: ScopeRule,
competitor_rule: ScopeRule,
},
#[error("the resolved Worker configuration or persisted history is invalid")] #[error("the resolved Worker configuration or persisted history is invalid")]
WorkerConfiguration, WorkerConfiguration,
#[error("the configured model provider is unavailable")] #[error("the configured model provider is unavailable")]
@@ -509,6 +521,15 @@ fn classify_store_startup_error(error: StandaloneStoreError) -> StandaloneStartu
fn classify_startup_error(error: WorkerBootstrapError) -> StandaloneStartupError { fn classify_startup_error(error: WorkerBootstrapError) -> StandaloneStartupError {
match error { match error {
WorkerBootstrapError::Worker(WorkerError::ScopeLock(ScopeLockError::WriteConflict {
competitor,
rule,
competitor_rule,
})) => StandaloneStartupError::ScopeConflict {
competitor,
requested_rule: rule,
competitor_rule,
},
WorkerBootstrapError::Worker(WorkerError::Provider(_)) => { WorkerBootstrapError::Worker(WorkerError::Provider(_)) => {
StandaloneStartupError::ModelProvider StandaloneStartupError::ModelProvider
} }
+59
View File
@@ -164,6 +164,65 @@ async fn in_process_host_runs_text_and_read_tool_then_shuts_down() {
host.shutdown().await.expect("graceful shutdown"); host.shutdown().await.expect("graceful shutdown");
} }
#[tokio::test]
async fn startup_preserves_occupied_scope_conflict_details() {
let temp = tempfile::tempdir().expect("tempdir");
let cwd = temp.path().join("project");
std::fs::create_dir(&cwd).expect("create project");
let first_launch = StandaloneLaunchConfig::new(
&cwd,
temp.path().join("first-state"),
manifest::ProfileSelector::Default,
"first-worker",
)
.resolve()
.expect("resolve first launch");
let first_host =
StandaloneHost::start_with_model_client(first_launch, ScriptedClient::new(Vec::new()))
.await
.expect("start first host");
let competitor = first_host.record().storage_key.clone();
let second_launch = StandaloneLaunchConfig::new(
&cwd,
temp.path().join("second-state"),
manifest::ProfileSelector::Default,
"second-worker",
)
.resolve()
.expect("resolve second launch");
let error =
StandaloneHost::start_with_model_client(second_launch, ScriptedClient::new(Vec::new()))
.await
.err()
.expect("occupied scope rejected");
first_host.shutdown().await.expect("shutdown first host");
let canonical_cwd = cwd.canonicalize().expect("canonical cwd");
match &error {
StandaloneStartupError::ScopeConflict {
competitor: actual_competitor,
requested_rule,
competitor_rule,
} => {
assert_eq!(actual_competitor, &competitor);
assert_eq!(requested_rule.target, canonical_cwd);
assert_eq!(competitor_rule.target, canonical_cwd);
}
other => panic!("expected scope conflict, got {other:?}"),
}
assert_eq!(
error.to_string(),
format!(
"requested scope `{}` conflicts with worker allocation `{competitor}` rule `{}`",
canonical_cwd.display(),
canonical_cwd.display()
)
);
}
#[tokio::test] #[tokio::test]
async fn state_store_failure_is_redacted_and_starts_no_controller() { async fn state_store_failure_is_redacted_and_starts_no_controller() {
let temp = tempfile::tempdir().expect("tempdir"); let temp = tempfile::tempdir().expect("tempdir");