feat(TASK-061): fix reconnect-flag race and completed-after-save hook ordering

- McpFactory::acquire now reads the live probe and the reconnect flag
  under one lock section so a racing first connect cannot be
  misreported as COYOTE_MCP_RECONNECT=true; the residual window across
  the spawn await (no singleflight) is documented. try_get_active,
  now unused, is removed.
- All three RAG sync funnels (init, init_with_config,
  refresh_document_paths) fire rag.sync.completed only after a
  successful save(), matching the documented contract; a save error
  routes through rag.sync.failed with COYOTE_ERROR before propagating.
- New funnel-level tests pin init_with_config's started-then-completed
  ordering, the failed event on a save error, and the failed event on
  the sync Err arm that a user abort surfaces through.
This commit is contained in:
Dark-Alex-17 committed 2026-09-17 13:43:16 -06:00
1 parent 04b983459b
commit dc2f26d0e6
2 files changed
+198 -39

No files matched your search

+15 -30
View File
@@ -81,11 +81,6 @@ pub struct McpFactory {
}
impl McpFactory {
pub fn try_get_active(&self, key: &McpServerKey) -> Option<Arc<ConnectedServer>> {
let map = self.active.lock();
map.get(key).and_then(|weak| weak.upgrade())
}
pub fn insert_active(&self, key: McpServerKey, handle: &Arc<ConnectedServer>) {
let mut map = self.active.lock();
map.insert(key, Arc::downgrade(handle));
@@ -101,15 +96,21 @@ impl McpFactory {
let key = McpServerKey::from_spec(name, spec);
// Reuse of a live server fires no hooks; only a real spawn below
// reports anything.
if let Some(existing) = self.try_get_active(&key) {
return Ok(existing);
}
// The live probe failed, so an entry still present for this key is a
// dead weak: the key was connected earlier in this process and the
// spawn below is a reconnect.
let reconnect = self.active.lock().contains_key(&key);
// reports anything. The live probe and the reconnect read share one
// lock section so a racing first connect that inserts between them
// cannot be misreported as a reconnect. The spawn await below stays
// outside the lock, so two racing first connects may still both
// spawn (no singleflight); the loser then reports a reconnect.
let reconnect = {
let map = self.active.lock();
if let Some(existing) = map.get(&key).and_then(|weak| weak.upgrade()) {
return Ok(existing);
}
// The live probe failed, so an entry still present for this key
// is a dead weak: the key was connected earlier in this process
// and the spawn below is a reconnect.
map.contains_key(&key)
};
let transport = transport_label(&spec.transport_type);
let handle = match spawn_server(name, spec, log_path).await {
@@ -357,22 +358,6 @@ mod tests {
}
}
#[test]
fn factory_try_get_active_returns_none_when_empty() {
let factory = McpFactory::default();
let spec = stdio_spec("cmd", None, None);
let key = McpServerKey::from_spec("s", &spec);
assert!(factory.try_get_active(&key).is_none());
}
#[test]
fn factory_try_get_active_returns_none_for_unknown_key() {
let factory = McpFactory::default();
let spec = stdio_spec("cmd", None, None);
let key = McpServerKey::from_spec("s", &spec);
assert!(factory.try_get_active(&key).is_none());
}
#[test]
fn factory_default_has_empty_active_map() {
let factory = McpFactory::default();