From 1957a655106c0ea4b10bd30d57fb203b4609fe26 Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Wed, 30 Sep 2026 08:59:10 -0700 Subject: [PATCH 1/2] fix: resolve a deployment group by name in the manager's list filter Co-Authored-By: Claude Opus 5.5 (1M context) --- .../alien-manager/src/routes/deployments.rs | 106 ++++++++++++++++-- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/crates/alien-manager/src/routes/deployments.rs b/crates/alien-manager/src/routes/deployments.rs index fd7fb90982..0a87acab03 100644 --- a/crates/alien-manager/src/routes/deployments.rs +++ b/crates/alien-manager/src/routes/deployments.rs @@ -238,6 +238,37 @@ fn representative_public_url( }) } +/// A group id as given, or the id of the one group with that name. A value that is neither is +/// returned unchanged, so the filter matches nothing, as before. +async fn resolve_deployment_group_id( + state: &AppState, + subject: &crate::auth::Subject, + id_or_name: String, +) -> Result { + if state + .deployment_store + .get_deployment_group(subject, &id_or_name) + .await? + .is_some() + { + return Ok(id_or_name); + } + let mut named = state + .deployment_store + .list_deployment_groups(subject) + .await? + .into_iter() + .filter(|group| group.name == id_or_name); + match (named.next(), named.next()) { + (Some(group), None) => Ok(group.id), + (Some(_), Some(_)) => Err(ErrorData::bad_request(format!( + "More than one deployment group is named '{id_or_name}'; filter by its ID" + )) + .into_generic()), + (None, _) => Ok(id_or_name), + } +} + // --- Router --- pub fn router() -> Router { @@ -602,6 +633,17 @@ async fn list_deployments( } }; + // The filter takes a group id or, as the platform's does, a group name: `/` + // deployment specs pass whatever the user typed, and `alien dev` names its group + // `local-dev` under a generated id. + let deployment_group_id = match deployment_group_id { + Some(id_or_name) => match resolve_deployment_group_id(&state, &subject, id_or_name).await { + Ok(id) => Some(id), + Err(e) => return e.into_response(), + }, + None => None, + }; + if query.name.is_some() && deployment_group_id.is_none() { return ErrorData::bad_request( "name filter requires deploymentGroupId unless the token is scoped to a deployment group", @@ -1097,6 +1139,21 @@ mod tests { .in_sequence(&mut sequence) .return_once(move |_, _| Ok(deployment_remains.then_some(deployment))); + let request = Request::builder() + .method("POST") + .uri("/v1/deployments/deployment-1/delete") + .header(http::header::AUTHORIZATION, "Bearer test-token") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from(r#"{"action":"cleanup"}"#)) + .expect("request should build"); + send(deployment_store, request).await + } + + /// Serve `request` through the deployments router over `deployment_store`. + async fn send( + deployment_store: MockDeploymentStore, + request: Request, + ) -> (StatusCode, serde_json::Value) { let temp = tempfile::tempdir().expect("test directory should be created"); let kv: Arc = Arc::new( LocalKv::new(temp.path().join("kv")) @@ -1143,13 +1200,6 @@ mod tests { ), import_registry: Arc::new(alien_infra::ImporterRegistry::built_in()), }; - let request = Request::builder() - .method("POST") - .uri("/v1/deployments/deployment-1/delete") - .header(http::header::AUTHORIZATION, "Bearer test-token") - .header(http::header::CONTENT_TYPE, "application/json") - .body(Body::from(r#"{"action":"cleanup"}"#)) - .expect("request should build"); let response = router() .with_state(state) .oneshot(request) @@ -1166,6 +1216,48 @@ mod tests { ) } + /// `/` specs send the group's name: `alien dev` names its group `local-dev` + /// under a generated id, and the list must find deployments in it. + #[tokio::test] + async fn list_resolves_a_deployment_group_by_name() { + let mut deployment_store = MockDeploymentStore::new(); + deployment_store + .expect_get_deployment_group() + .returning(|_, _| Ok(None)); + deployment_store + .expect_list_deployment_groups() + .returning(|_| { + Ok(vec![crate::traits::DeploymentGroupRecord { + id: "dg_generated".to_string(), + workspace_id: "default".to_string(), + project_id: "default".to_string(), + name: "local-dev".to_string(), + max_deployments: 100, + deployment_count: 1, + created_at: Utc::now(), + }]) + }); + deployment_store + .expect_list_deployments() + .withf(|_, filter| { + filter.deployment_group_id.as_deref() == Some("dg_generated") + && filter.name.as_deref() == Some("deployment-1") + }) + .times(1) + .returning(|_, _| Ok(vec![deployment_record()])); + + let request = Request::builder() + .method("GET") + .uri("/v1/deployments?deploymentGroupId=local-dev&name=deployment-1") + .header(http::header::AUTHORIZATION, "Bearer test-token") + .body(Body::empty()) + .expect("request should build"); + let (status, json) = send(deployment_store, request).await; + + assert_eq!(status, StatusCode::OK, "{json}"); + assert_eq!(json["items"][0]["id"], "deployment-1"); + } + #[tokio::test] async fn cleanup_route_requires_setup_teardown_while_deployment_remains() { let (status, json) = cleanup_route_response(true).await; From 76bab34aff1360f5cfee6ea82533bafaac4ca8ce Mon Sep 17 00:00:00 2001 From: Alon Gubkin Date: Wed, 30 Sep 2026 17:16:51 -0700 Subject: [PATCH 2/2] test: the resolved group's deployment comes back Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/alien-manager/src/routes/deployments.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/alien-manager/src/routes/deployments.rs b/crates/alien-manager/src/routes/deployments.rs index 0a87acab03..8b9d08216d 100644 --- a/crates/alien-manager/src/routes/deployments.rs +++ b/crates/alien-manager/src/routes/deployments.rs @@ -1244,7 +1244,12 @@ mod tests { && filter.name.as_deref() == Some("deployment-1") }) .times(1) - .returning(|_, _| Ok(vec![deployment_record()])); + .returning(|_, _| { + Ok(vec![DeploymentRecord { + deployment_group_id: "dg_generated".to_string(), + ..deployment_record() + }]) + }); let request = Request::builder() .method("GET") @@ -1256,6 +1261,7 @@ mod tests { assert_eq!(status, StatusCode::OK, "{json}"); assert_eq!(json["items"][0]["id"], "deployment-1"); + assert_eq!(json["items"][0]["deploymentGroupId"], "dg_generated"); } #[tokio::test]