|
3 | 3 | //! Tests for feature set CRUD, builtin types (All, Default, ServerAll), |
4 | 4 | //! and feature member composition. |
5 | 5 |
|
6 | | -use mcpmux_core::domain::{FeatureSetType, MemberMode}; |
| 6 | +use mcpmux_core::domain::{FeatureSetMember, FeatureSetType, MemberMode}; |
7 | 7 | use mcpmux_core::repository::{FeatureSetRepository, SpaceRepository}; |
8 | 8 | use mcpmux_storage::{SqliteFeatureSetRepository, SqliteSpaceRepository}; |
9 | 9 | use std::sync::Arc; |
@@ -112,6 +112,55 @@ async fn test_update_feature_set() { |
112 | 112 | assert_eq!(loaded.description, Some("New description".to_string())); |
113 | 113 | } |
114 | 114 |
|
| 115 | +/// The builtin Starter's identity is locked: `update()` must ignore name / |
| 116 | +/// description / icon changes on a builtin row (it's the default fallback, not |
| 117 | +/// renamable) while still applying MEMBER changes — that's how the member |
| 118 | +/// editor (which routes through `update()`) keeps working for the Starter. |
| 119 | +#[tokio::test] |
| 120 | +async fn test_update_locks_builtin_identity_but_allows_member_edits() { |
| 121 | + let test_db = TestDatabase::new(); |
| 122 | + let db = Arc::new(Mutex::new(test_db.db)); |
| 123 | + let feature_repo = SqliteFeatureSetRepository::new(Arc::clone(&db)); |
| 124 | + let space_repo = SqliteSpaceRepository::new(db); |
| 125 | + |
| 126 | + let space = fixtures::test_space("Test Space"); |
| 127 | + SpaceRepository::create(&space_repo, &space).await.unwrap(); |
| 128 | + |
| 129 | + // Auto-seed the builtin Starter for this Space. |
| 130 | + FeatureSetRepository::ensure_builtin_for_space(&feature_repo, &space.id.to_string()) |
| 131 | + .await |
| 132 | + .unwrap(); |
| 133 | + let mut starter = |
| 134 | + FeatureSetRepository::get_starter_for_space(&feature_repo, &space.id.to_string()) |
| 135 | + .await |
| 136 | + .unwrap() |
| 137 | + .expect("starter seeded"); |
| 138 | + let original_name = starter.name.clone(); |
| 139 | + let original_desc = starter.description.clone(); |
| 140 | + |
| 141 | + // Attempt to rename + re-describe the builtin AND add a member at once. |
| 142 | + starter.name = "Hacked Name".to_string(); |
| 143 | + starter.description = Some("hacked description".to_string()); |
| 144 | + starter.members = vec![FeatureSetMember::include_feature(&starter.id, "feat-x")]; |
| 145 | + FeatureSetRepository::update(&feature_repo, &starter) |
| 146 | + .await |
| 147 | + .expect("update should succeed"); |
| 148 | + |
| 149 | + let loaded = FeatureSetRepository::get_with_members(&feature_repo, &starter.id) |
| 150 | + .await |
| 151 | + .unwrap() |
| 152 | + .unwrap(); |
| 153 | + // Identity preserved... |
| 154 | + assert_eq!(loaded.name, original_name, "builtin name must not change"); |
| 155 | + assert_eq!( |
| 156 | + loaded.description, original_desc, |
| 157 | + "builtin description must not change" |
| 158 | + ); |
| 159 | + // ...but the member edit went through. |
| 160 | + assert_eq!(loaded.members.len(), 1, "members must stay editable"); |
| 161 | + assert_eq!(loaded.members[0].member_id, "feat-x"); |
| 162 | +} |
| 163 | + |
115 | 164 | #[tokio::test] |
116 | 165 | async fn test_delete_feature_set() { |
117 | 166 | let test_db = TestDatabase::new(); |
|
0 commit comments