huskies: merge 1132 story Chat-bot proxy reads stale gateway_project_urls BTreeMap instead of live store (1122 missed this seam)
This commit is contained in:
@@ -88,10 +88,6 @@ pub struct BotContext {
|
||||
/// In gateway mode: the currently active project (shared with the gateway HTTP handler).
|
||||
/// `None` in standalone single-project mode.
|
||||
pub gateway_active_project: Option<Arc<RwLock<String>>>,
|
||||
/// In gateway mode: mapping of project name → base URL (e.g. `"http://localhost:3001"`).
|
||||
/// Used to proxy bot commands to the active project over WebSocket (`/ws`).
|
||||
/// Empty in standalone mode.
|
||||
pub gateway_project_urls: BTreeMap<String, String>,
|
||||
/// In gateway mode: shared live projects map from [`GatewayState`].
|
||||
///
|
||||
/// The `new project` command writes here so HTTP handlers see the new entry
|
||||
@@ -130,7 +126,12 @@ impl BotContext {
|
||||
pub async fn active_project_url(&self) -> Option<String> {
|
||||
let ap = self.gateway_active_project.as_ref()?;
|
||||
let name = ap.read().await.clone();
|
||||
self.gateway_project_urls.get(&name).cloned()
|
||||
let store = self.gateway_projects_store.as_ref()?;
|
||||
store
|
||||
.read()
|
||||
.await
|
||||
.get(&name)
|
||||
.and_then(|entry| entry.url.clone())
|
||||
}
|
||||
|
||||
/// Proxy a bot command to the active project over a WebSocket RPC call.
|
||||
@@ -266,7 +267,9 @@ mod tests {
|
||||
fn test_bot_context(
|
||||
services: Arc<Services>,
|
||||
gateway_active_project: Option<Arc<RwLock<String>>>,
|
||||
gateway_project_urls: BTreeMap<String, String>,
|
||||
gateway_projects_store: Option<
|
||||
Arc<RwLock<BTreeMap<String, crate::service::gateway::config::ProjectEntry>>>,
|
||||
>,
|
||||
) -> BotContext {
|
||||
BotContext {
|
||||
services,
|
||||
@@ -286,8 +289,7 @@ mod tests {
|
||||
std::path::PathBuf::from("/tmp/timers.json"),
|
||||
)),
|
||||
gateway_active_project,
|
||||
gateway_project_urls,
|
||||
gateway_projects_store: None,
|
||||
gateway_projects_store,
|
||||
handled_incoming_event_ids: Arc::new(TokioMutex::new(SeenEventIds::new(
|
||||
SEEN_EVENT_IDS_CAP,
|
||||
))),
|
||||
@@ -304,7 +306,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn effective_project_root_standalone_returns_project_root() {
|
||||
let services = test_services(PathBuf::from("/projects/myapp"));
|
||||
let ctx = test_bot_context(services, None, BTreeMap::new());
|
||||
let ctx = test_bot_context(services, None, None);
|
||||
assert_eq!(
|
||||
ctx.effective_project_root().await,
|
||||
PathBuf::from("/projects/myapp")
|
||||
@@ -315,14 +317,7 @@ mod tests {
|
||||
async fn effective_project_root_gateway_uses_active_project_subdir() {
|
||||
let services = test_services(PathBuf::from("/gateway"));
|
||||
let active = Arc::new(RwLock::new("huskies".to_string()));
|
||||
let ctx = test_bot_context(
|
||||
services,
|
||||
Some(Arc::clone(&active)),
|
||||
BTreeMap::from([
|
||||
("huskies".into(), "http://localhost:3001".into()),
|
||||
("robot-studio".into(), "http://localhost:3002".into()),
|
||||
]),
|
||||
);
|
||||
let ctx = test_bot_context(services, Some(Arc::clone(&active)), None);
|
||||
assert_eq!(
|
||||
ctx.effective_project_root().await,
|
||||
PathBuf::from("/gateway/huskies")
|
||||
@@ -333,14 +328,7 @@ mod tests {
|
||||
async fn effective_project_root_gateway_reflects_project_switch() {
|
||||
let services = test_services(PathBuf::from("/gateway"));
|
||||
let active = Arc::new(RwLock::new("huskies".to_string()));
|
||||
let ctx = test_bot_context(
|
||||
services,
|
||||
Some(Arc::clone(&active)),
|
||||
BTreeMap::from([
|
||||
("huskies".into(), "http://localhost:3001".into()),
|
||||
("robot-studio".into(), "http://localhost:3002".into()),
|
||||
]),
|
||||
);
|
||||
let ctx = test_bot_context(services, Some(Arc::clone(&active)), None);
|
||||
|
||||
assert_eq!(
|
||||
ctx.effective_project_root().await,
|
||||
@@ -416,7 +404,7 @@ mod tests {
|
||||
#[test]
|
||||
fn bot_context_has_no_require_verified_devices_field() {
|
||||
let services = test_services(PathBuf::from("/tmp"));
|
||||
let ctx = test_bot_context(services, None, BTreeMap::new());
|
||||
let ctx = test_bot_context(services, None, None);
|
||||
let _cloned = ctx.clone();
|
||||
}
|
||||
|
||||
@@ -463,11 +451,16 @@ mod tests {
|
||||
let base_url = format!("http://127.0.0.1:{port}");
|
||||
let services = test_services(PathBuf::from("/gateway"));
|
||||
let active = Arc::new(RwLock::new("huskies".to_string()));
|
||||
let ctx = test_bot_context(
|
||||
services,
|
||||
Some(Arc::clone(&active)),
|
||||
BTreeMap::from([("huskies".into(), base_url)]),
|
||||
);
|
||||
let store = Arc::new(RwLock::new(BTreeMap::from([(
|
||||
"huskies".to_string(),
|
||||
crate::service::gateway::config::ProjectEntry {
|
||||
url: Some(base_url),
|
||||
auth_token: None,
|
||||
ssh_port: None,
|
||||
host_path: None,
|
||||
},
|
||||
)])));
|
||||
let ctx = test_bot_context(services, Some(Arc::clone(&active)), Some(store));
|
||||
|
||||
let result = ctx.proxy_bot_command("status", "").await;
|
||||
assert_eq!(
|
||||
@@ -478,4 +471,45 @@ mod tests {
|
||||
|
||||
server.await.unwrap();
|
||||
}
|
||||
|
||||
/// Regression test for story 1132: `active_project_url` must read from the
|
||||
/// live `gateway_projects_store`, not a stale snapshot frozen at bot startup.
|
||||
/// Adding a project to the store after `BotContext` is created must be
|
||||
/// visible immediately — no restart required.
|
||||
#[tokio::test]
|
||||
async fn active_project_url_reflects_runtime_added_project() {
|
||||
let store: Arc<RwLock<BTreeMap<String, crate::service::gateway::config::ProjectEntry>>> =
|
||||
Arc::new(RwLock::new(BTreeMap::new()));
|
||||
let active = Arc::new(RwLock::new("new-project".to_string()));
|
||||
let services = test_services(PathBuf::from("/gateway"));
|
||||
let ctx = test_bot_context(
|
||||
services,
|
||||
Some(Arc::clone(&active)),
|
||||
Some(Arc::clone(&store)),
|
||||
);
|
||||
|
||||
// Store is empty — must return None.
|
||||
assert!(
|
||||
ctx.active_project_url().await.is_none(),
|
||||
"URL must be None when store is empty"
|
||||
);
|
||||
|
||||
// Insert the entry at runtime (simulates `new project` command).
|
||||
store.write().await.insert(
|
||||
"new-project".to_string(),
|
||||
crate::service::gateway::config::ProjectEntry {
|
||||
url: Some("http://localhost:3099".to_string()),
|
||||
auth_token: None,
|
||||
ssh_port: None,
|
||||
host_path: None,
|
||||
},
|
||||
);
|
||||
|
||||
// Now the live store has the entry — active_project_url must see it.
|
||||
assert_eq!(
|
||||
ctx.active_project_url().await.as_deref(),
|
||||
Some("http://localhost:3099"),
|
||||
"URL must be visible after runtime insertion without bot restart"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user