1use std::sync::Arc;
65
66use crate::backend::storage::store::{Bundle, Store};
67use crate::registry::{resolve_shared_definitions_dir, DefinitionStore};
68
69use super::{Migration, MigrationContext, MigrationError, MigrationScope};
70
71pub struct M0021BackfillAgentBundles;
72
73const FALLBACK_PROVIDER: &str = "claude";
78const FALLBACK_MODEL: &str = "anthropic";
79
80fn resolve_backfill_provider_and_model(provider: &str, model_vendor_base_url: &str) -> (String, String) {
89 if provider.is_empty() {
90 return (FALLBACK_PROVIDER.to_string(), FALLBACK_MODEL.to_string());
91 }
92 let vendor = Store::resolve_effective_vendor(provider, model_vendor_base_url);
93 (provider.to_string(), vendor)
94}
95
96pub(super) fn resolve_bundle_store(ctx: &MigrationContext, mstore: &Arc<Store>) -> Arc<Store> {
103 match Store::open_shared(&ctx.shared_store_path) {
104 Ok(shared) => Arc::new(shared),
105 Err(e) => {
106 tracing::warn!(
107 error = %e,
108 path = %ctx.shared_store_path.display(),
109 "backfill_agent_bundles: shared store unavailable, falling back to channel store for bundle writes"
110 );
111 mstore.clone()
112 }
113 }
114}
115
116impl Migration for M0021BackfillAgentBundles {
117 fn id(&self) -> &'static str { "0021_backfill_agent_bundles" }
118 fn scope(&self) -> MigrationScope { MigrationScope::Channel }
119 fn description(&self) -> &'static str {
120 "Provision a dedicated ABF bundle for every agent definition lacking one"
121 }
122
123 fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError> {
124 if !ctx.channel_store_path.exists() {
125 return Ok(());
126 }
127 let mstore = Arc::new(
128 Store::open(&ctx.channel_store_path)
129 .map_err(|e| MigrationError(format!("backfill_agent_bundles: open mstore: {}", e)))?,
130 );
131 if let Some(def_dir) = resolve_shared_definitions_dir() {
138 match DefinitionStore::open(def_dir) {
139 Ok(def_store) => mstore.set_def_registry(Arc::new(def_store)),
140 Err(e) => tracing::warn!(error = %e, "backfill_agent_bundles: failed to open global def registry, backfilling local-only"),
141 }
142 } else {
143 tracing::warn!("backfill_agent_bundles: could not resolve global def registry dir, backfilling local-only");
144 }
145
146 let bundle_store = resolve_bundle_store(ctx, &mstore);
147 let sidecar = crate::backend::agent_bundle_sidecar::open_global();
152
153 let defs = mstore
154 .agent_def_list()
155 .map_err(|e| MigrationError(format!("backfill_agent_bundles: list defs: {}", e)))?;
156 let now = std::time::SystemTime::now()
157 .duration_since(std::time::UNIX_EPOCH)
158 .map(|d| d.as_millis() as i64)
159 .unwrap_or(0);
160
161 for def in defs {
162 if !def.memory_id.is_empty() {
163 continue;
164 }
165 let local = mstore
171 .agent_def_has_local_row(&def.id)
172 .map_err(|e| MigrationError(format!("backfill_agent_bundles: look up {}: {}", def.id, e)))?;
173 if !local {
174 tracing::debug!(agent_id = %def.id, "backfill_agent_bundles: definition only in the global registry; nothing here to bind a bundle to");
175 continue;
176 }
177 use crate::backend::agent_bundle_sidecar as sc;
178 let recorded = sidecar.as_ref().and_then(|fs| sc::get(fs, &def.id).ok().flatten());
179 if let Some(existing) = recorded.filter(|id| matches!(bundle_store.bundle_get(id), Ok(Some(_)))) {
180 mstore
181 .agent_def_set_memory_id_if_empty(&def.id, &existing)
182 .map_err(|e| MigrationError(format!("backfill_agent_bundles: bind {}: {}", def.id, e)))?;
183 continue;
184 }
185 let bundle_id = uuid::Uuid::new_v4().to_string();
186 let (provider, model) = resolve_backfill_provider_and_model(&def.provider, &def.model_vendor_base_url);
187 let name = bundle_store
196 .resolve_unique_bundle_name(&format!("{} — ABF", def.name))
197 .map_err(|e| MigrationError(format!("backfill_agent_bundles: resolve unique bundle name for {}: {}", def.id, e)))?;
198 let bundle = Bundle {
199 id: bundle_id.clone(),
200 name,
201 description: String::new(),
202 is_blank: false,
203 is_global: false,
204 provider,
205 model,
206 instructions: String::new(),
207 instructions_by_provider: "{}".to_string(),
208 context_files: "[]".to_string(),
209 mcp_servers: "[]".to_string(),
210 skills: "[]".to_string(),
211 sort_order: 0,
212 created_at: now,
213 updated_at: now,
214 is_system: false,
215 };
216 bundle_store
217 .bundle_upsert(&bundle)
218 .map_err(|e| MigrationError(format!("backfill_agent_bundles: create bundle for {}: {}", def.id, e)))?;
219 let mut bundle_id = bundle_id;
222 if let Some(fs) = sidecar.as_ref() {
223 let live = |id: &str| matches!(bundle_store.bundle_get(id), Ok(Some(_)));
226 match sc::claim_unless_live(fs, &def.id, &bundle_id, &live) {
227 Ok(winner) if winner != bundle_id && matches!(bundle_store.bundle_get(&winner), Ok(Some(_))) => {
228 if let Err(e) = bundle_store.bundle_delete(&bundle_id) {
229 tracing::warn!(
230 agent_id = %def.id,
231 bundle_id = %bundle_id,
232 error = %e,
233 "backfill_agent_bundles: another channel's bundle won; this duplicate could not be deleted and is left unbound"
234 );
235 }
236 bundle_id = winner;
237 }
238 Ok(_) => {}
239 Err(e) => tracing::warn!(agent_id = %def.id, error = %e, "backfill_agent_bundles: bundle sidecar not recorded"),
240 }
241 }
242
243 let applied = mstore
244 .agent_def_set_memory_id_if_empty(&def.id, &bundle_id)
245 .map_err(|e| MigrationError(format!("backfill_agent_bundles: bind {}: {}", def.id, e)))?;
246 if !applied {
247 tracing::warn!(
254 agent_id = %def.id,
255 bundle_id = %bundle_id,
256 "backfill_agent_bundles: definition only exists in the global registry — \
257 cannot bind memory_id there yet (DefinitionRecordV1 gap); bundle created but unbound"
258 );
259 }
260 }
261 Ok(())
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::backend::storage::agents::AgentDefinition;
269
270 use crate::test_support::ISOLATED_AUTH_ENV_LOCK as ENV_GUARD;
283
284 fn with_isolated_home<T>(f: impl FnOnce(&std::path::Path) -> T) -> T {
285 let _guard = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
286 let home_tmp = tempfile::tempdir().unwrap();
287 std::env::set_var("AGENTMUX_HOME_OVERRIDE", home_tmp.path().to_str().unwrap());
288 let result = f(home_tmp.path());
289 std::env::remove_var("AGENTMUX_HOME_OVERRIDE");
290 result
291 }
292
293 fn ctx_for(channel_path: &std::path::Path, shared_path: &std::path::Path) -> MigrationContext {
299 MigrationContext {
300 home: std::env::temp_dir(),
301 data_dir: std::env::temp_dir(),
302 shared_store_path: shared_path.to_path_buf(),
303 channel_store_path: channel_path.to_path_buf(),
304 }
305 }
306
307 fn insert_def(mstore: &Store, name: &str) -> String {
308 let mut def = AgentDefinition {
309 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
310 id: format!("test-{name}"),
311 slug: String::new(),
312 name: name.to_string(),
313 icon: String::new(),
314 provider: "claude".to_string(),
315 description: String::new(),
316 working_directory: String::new(),
317 shell: String::new(),
318 provider_flags: String::new(),
319 auto_start: 0,
320 restart_on_crash: 0,
321 idle_timeout_minutes: 0,
322 created_at: 1,
323 agent_type: "host".to_string(),
324 environment: String::new(),
325 agent_bus_id: String::new(),
326 is_seeded: 0,
327 accounts: String::new(),
328 parent_id: String::new(),
329 branch_label: String::new(),
330 updated_at: 1,
331 user_hidden: 0,
332 container_image: String::new(),
333 container_volumes: "[]".to_string(),
334 container_name: String::new(),
335 use_ambient_login: 0,
336 auto_continue_enabled: 0,
337 model_vendor_base_url: String::new(),
338 memory_id: String::new(),
339 };
340 mstore.agent_def_insert(&mut def).unwrap();
341 def.id
342 }
343
344 fn insert_def_with_provider(mstore: &Store, name: &str, provider: &str) -> String {
345 let mut def = AgentDefinition {
346 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
347 id: format!("test-{name}"),
348 slug: String::new(),
349 name: name.to_string(),
350 icon: String::new(),
351 provider: provider.to_string(),
352 description: String::new(),
353 working_directory: String::new(),
354 shell: String::new(),
355 provider_flags: String::new(),
356 auto_start: 0,
357 restart_on_crash: 0,
358 idle_timeout_minutes: 0,
359 created_at: 1,
360 agent_type: "host".to_string(),
361 environment: String::new(),
362 agent_bus_id: String::new(),
363 is_seeded: 0,
364 accounts: String::new(),
365 parent_id: String::new(),
366 branch_label: String::new(),
367 updated_at: 1,
368 user_hidden: 0,
369 container_image: String::new(),
370 container_volumes: "[]".to_string(),
371 container_name: String::new(),
372 use_ambient_login: 0,
373 auto_continue_enabled: 0,
374 model_vendor_base_url: String::new(),
375 memory_id: String::new(),
376 };
377 mstore.agent_def_insert(&mut def).unwrap();
378 def.id
379 }
380
381 #[test]
385 fn a_second_channel_binds_the_bundle_the_first_recorded() {
386 with_isolated_home(|_home| {
387 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
388 let (ch1, ch2) = (tempfile::NamedTempFile::new().unwrap(), tempfile::NamedTempFile::new().unwrap());
389 let (s1, s2) = (Store::open(ch1.path()).unwrap(), Store::open(ch2.path()).unwrap());
390 let id = insert_def(&s1, "RoamingAgent");
391 assert_eq!(insert_def(&s2, "RoamingAgent"), id, "same agent in both channels");
392
393 M0021BackfillAgentBundles.up(&ctx_for(ch1.path(), shared_tmp.path())).unwrap();
394 M0021BackfillAgentBundles.up(&ctx_for(ch2.path(), shared_tmp.path())).unwrap();
395
396 let b1 = s1.agent_def_get(&id).unwrap().unwrap().memory_id;
397 let b2 = s2.agent_def_get(&id).unwrap().unwrap().memory_id;
398 assert!(!b1.is_empty());
399 assert_eq!(b1, b2, "both channels bind one bundle");
400 let shared = Store::open_shared(shared_tmp.path()).unwrap();
401 let abf: Vec<String> =
402 shared.bundle_list().unwrap().into_iter().map(|b| b.name).filter(|n| n.contains("RoamingAgent")).collect();
403 assert_eq!(abf, vec!["RoamingAgent — ABF".to_string()], "no second bundle minted");
404 });
405 }
406
407 #[test]
410 fn a_global_only_definition_gets_no_orphaned_bundle() {
411 with_isolated_home(|home| {
412 use crate::registry::{DefinitionRecord, DefinitionRecordV1, DEF_MAX_SUPPORTED_SCHEMA};
413 let def_dir = home.join("shared").join("agents").join("definitions");
414 crate::registry::DefinitionStore::open(def_dir)
415 .unwrap()
416 .upsert(&DefinitionRecord {
417 schema_version: DEF_MAX_SUPPORTED_SCHEMA,
418 data: DefinitionRecordV1 {
419 id: "test-GlobalOnly".to_string(),
420 name: "GlobalOnly".to_string(),
421 provider: "claude".to_string(),
422 ..Default::default()
423 },
424 })
425 .unwrap();
426 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
427 let ch = tempfile::NamedTempFile::new().unwrap();
428 let s = Store::open(ch.path()).unwrap();
429 assert!(!s.agent_def_has_local_row("test-GlobalOnly").unwrap());
430
431 M0021BackfillAgentBundles.up(&ctx_for(ch.path(), shared_tmp.path())).unwrap();
432
433 let shared = Store::open_shared(shared_tmp.path()).unwrap();
434 let abf: Vec<String> =
435 shared.bundle_list().unwrap().into_iter().map(|b| b.name).filter(|n| n.contains("GlobalOnly")).collect();
436 assert!(abf.is_empty(), "minted an orphan: {abf:?}");
437 });
438 }
439
440 #[test]
444 fn a_recorded_bundle_missing_here_is_replaced_then_reused() {
445 with_isolated_home(|_home| {
446 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
447 let (ch1, ch2) = (tempfile::NamedTempFile::new().unwrap(), tempfile::NamedTempFile::new().unwrap());
448 let (s1, s2) = (Store::open(ch1.path()).unwrap(), Store::open(ch2.path()).unwrap());
449 let id = insert_def(&s1, "PinnedAgent");
450 insert_def(&s2, "PinnedAgent");
451 let global = crate::backend::agent_bundle_sidecar::open_global().unwrap();
452 crate::backend::agent_bundle_sidecar::claim(&global, &id, "bundle-in-an-isolated-store").unwrap();
453
454 M0021BackfillAgentBundles.up(&ctx_for(ch1.path(), shared_tmp.path())).unwrap();
455 let b1 = s1.agent_def_get(&id).unwrap().unwrap().memory_id;
456 assert!(!b1.is_empty() && b1 != "bundle-in-an-isolated-store");
457 assert_eq!(crate::backend::agent_bundle_sidecar::get(&global, &id).unwrap().as_deref(), Some(b1.as_str()));
458
459 M0021BackfillAgentBundles.up(&ctx_for(ch2.path(), shared_tmp.path())).unwrap();
460 assert_eq!(s2.agent_def_get(&id).unwrap().unwrap().memory_id, b1, "the second channel reuses it");
461 let shared = Store::open_shared(shared_tmp.path()).unwrap();
462 let abf = shared.bundle_list().unwrap().into_iter().filter(|b| b.name.contains("PinnedAgent")).count();
463 assert_eq!(abf, 1, "one bundle, not one per channel");
464 });
465 }
466
467 #[test]
468 fn backfills_a_definition_with_no_bundle() {
469 with_isolated_home(|_home| {
470 let tmp = tempfile::NamedTempFile::new().unwrap();
471 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
472 let mstore = Store::open(tmp.path()).unwrap();
473 let id = insert_def(&mstore, "Plain Agent");
474
475 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
476
477 let def = mstore.agent_def_get(&id).unwrap().unwrap();
478 assert!(!def.memory_id.is_empty(), "expected memory_id to be backfilled");
479 let shared = Store::open_shared(shared_tmp.path()).unwrap();
480 let bundle = shared.bundle_get(&def.memory_id).unwrap().unwrap();
481 assert_eq!(bundle.provider, "claude");
484 assert_eq!(bundle.model, "anthropic");
485 assert!(bundle.name.contains("Plain Agent"));
486 assert!(!bundle.is_blank);
487 });
488 }
489
490 #[test]
496 fn backfilled_bundle_lands_in_the_shared_store_not_the_channel_store() {
497 with_isolated_home(|_home| {
498 let tmp = tempfile::NamedTempFile::new().unwrap();
499 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
500 let mstore = Store::open(tmp.path()).unwrap();
501 let id = insert_def(&mstore, "Shared Store Agent");
502
503 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
504
505 let def = mstore.agent_def_get(&id).unwrap().unwrap();
506 assert!(!def.memory_id.is_empty());
507
508 let channel_bundle_count = mstore.bundle_list().unwrap().iter().filter(|b| !b.is_blank).count();
512 assert_eq!(channel_bundle_count, 0, "bundle must not be written into the channel store");
513
514 let shared = Store::open_shared(shared_tmp.path()).unwrap();
516 assert!(shared.bundle_get(&def.memory_id).unwrap().is_some(), "bundle must be reachable via the shared store");
517 });
518 }
519
520 #[test]
529 fn backfill_derives_provider_and_model_from_the_definitions_own_provider_not_a_hardcoded_default() {
530 with_isolated_home(|_home| {
531 let tmp = tempfile::NamedTempFile::new().unwrap();
532 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
533 let mstore = Store::open(tmp.path()).unwrap();
534 let id = insert_def_with_provider(&mstore, "Codex Agent", "codex");
535
536 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
537
538 let def = mstore.agent_def_get(&id).unwrap().unwrap();
539 let shared = Store::open_shared(shared_tmp.path()).unwrap();
540 let bundle = shared.bundle_get(&def.memory_id).unwrap().unwrap();
541 assert_eq!(bundle.provider, "codex", "must carry the agent's OWN provider, not claude");
542 assert_eq!(bundle.model, "openai", "vendor derived from codex's supported_vendors[0]");
543 });
544 }
545
546 #[test]
551 fn backfill_uses_custom_vendor_when_the_definition_has_a_model_vendor_base_url_override() {
552 with_isolated_home(|_home| {
553 let tmp = tempfile::NamedTempFile::new().unwrap();
554 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
555 let mstore = Store::open(tmp.path()).unwrap();
556 let id = insert_def_with_provider(&mstore, "Custom Vendor Agent", "claude");
557 {
558 let mut def = mstore.agent_def_get(&id).unwrap().unwrap();
559 def.model_vendor_base_url = "https://my-proxy.example.com".to_string();
560 mstore.agent_def_update(&mut def).unwrap();
561 }
562
563 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
564
565 let def = mstore.agent_def_get(&id).unwrap().unwrap();
566 let shared = Store::open_shared(shared_tmp.path()).unwrap();
567 let bundle = shared.bundle_get(&def.memory_id).unwrap().unwrap();
568 assert_eq!(bundle.provider, "claude");
569 assert_eq!(bundle.model, "custom", "a vendor override must backfill to \"custom\", not the provider default");
570 });
571 }
572
573 #[test]
574 fn backfill_falls_back_to_claude_anthropic_only_when_the_definition_has_no_provider_at_all() {
575 with_isolated_home(|_home| {
576 let tmp = tempfile::NamedTempFile::new().unwrap();
577 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
578 let mstore = Store::open(tmp.path()).unwrap();
579 let id = insert_def_with_provider(&mstore, "No Provider Agent", "");
580
581 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
582
583 let def = mstore.agent_def_get(&id).unwrap().unwrap();
584 let shared = Store::open_shared(shared_tmp.path()).unwrap();
585 let bundle = shared.bundle_get(&def.memory_id).unwrap().unwrap();
586 assert_eq!(bundle.provider, FALLBACK_PROVIDER);
587 assert_eq!(bundle.model, FALLBACK_MODEL);
588 });
589 }
590
591 #[test]
592 fn leaves_an_already_bound_definition_untouched() {
593 with_isolated_home(|_home| {
594 let tmp = tempfile::NamedTempFile::new().unwrap();
595 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
596 let mstore = Store::open(tmp.path()).unwrap();
597 let id = insert_def(&mstore, "Already Bound");
598 mstore.agent_def_set_memory_id_if_empty(&id, "pre-existing-bundle").unwrap();
599
600 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
601
602 let def = mstore.agent_def_get(&id).unwrap().unwrap();
603 assert_eq!(def.memory_id, "pre-existing-bundle", "must not overwrite an existing binding");
604 });
605 }
606
607 #[test]
608 fn rerun_is_idempotent_no_duplicate_bundles() {
609 with_isolated_home(|_home| {
610 let tmp = tempfile::NamedTempFile::new().unwrap();
611 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
612 let mstore = Store::open(tmp.path()).unwrap();
613 let id = insert_def(&mstore, "Rerun Agent");
614
615 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
616 let first_bundle_id = mstore.agent_def_get(&id).unwrap().unwrap().memory_id;
617
618 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
619 let second_bundle_id = mstore.agent_def_get(&id).unwrap().unwrap().memory_id;
620
621 assert_eq!(first_bundle_id, second_bundle_id, "rerun must not replace the bound bundle");
622 let shared = Store::open_shared(shared_tmp.path()).unwrap();
627 let non_blank_count = shared
628 .bundle_list()
629 .unwrap()
630 .iter()
631 .filter(|b| !b.is_blank)
632 .count();
633 assert_eq!(non_blank_count, 1, "rerun must not create a second orphaned bundle");
634 });
635 }
636
637 #[test]
638 fn backfills_multiple_definitions_with_distinct_bundles() {
639 with_isolated_home(|_home| {
640 let tmp = tempfile::NamedTempFile::new().unwrap();
641 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
642 let mstore = Store::open(tmp.path()).unwrap();
643 let a = insert_def(&mstore, "Agent A");
644 let b = insert_def(&mstore, "Agent B");
645
646 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
647
648 let bundle_a = mstore.agent_def_get(&a).unwrap().unwrap().memory_id;
649 let bundle_b = mstore.agent_def_get(&b).unwrap().unwrap().memory_id;
650 assert!(!bundle_a.is_empty());
651 assert!(!bundle_b.is_empty());
652 assert_ne!(bundle_a, bundle_b, "each agent must get its OWN dedicated bundle");
653 });
654 }
655
656 #[test]
663 fn backfills_both_agents_when_two_definitions_share_a_display_name() {
664 with_isolated_home(|_home| {
665 let tmp = tempfile::NamedTempFile::new().unwrap();
666 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
667 let mstore = Store::open(tmp.path()).unwrap();
668 let mut def_a = AgentDefinition {
669 conversation_visibility: crate::backend::storage::agents::default_conversation_visibility(),
670 id: "test-twin-a".to_string(),
671 slug: String::new(),
672 name: "Twin".to_string(),
673 icon: String::new(),
674 provider: "claude".to_string(),
675 description: String::new(),
676 working_directory: String::new(),
677 shell: String::new(),
678 provider_flags: String::new(),
679 auto_start: 0,
680 restart_on_crash: 0,
681 idle_timeout_minutes: 0,
682 created_at: 1,
683 agent_type: "host".to_string(),
684 environment: String::new(),
685 agent_bus_id: String::new(),
686 is_seeded: 0,
687 accounts: String::new(),
688 parent_id: String::new(),
689 branch_label: String::new(),
690 updated_at: 1,
691 user_hidden: 0,
692 container_image: String::new(),
693 container_volumes: "[]".to_string(),
694 container_name: String::new(),
695 use_ambient_login: 0,
696 auto_continue_enabled: 0,
697 model_vendor_base_url: String::new(),
698 memory_id: String::new(),
699 };
700 let mut def_b = def_a.clone();
701 def_b.id = "test-twin-b".to_string();
702 mstore.agent_def_insert(&mut def_a).unwrap();
703 mstore.agent_def_insert(&mut def_b).unwrap();
704
705 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), shared_tmp.path())).unwrap();
706
707 let bound_a = mstore.agent_def_get(&def_a.id).unwrap().unwrap().memory_id;
708 let bound_b = mstore.agent_def_get(&def_b.id).unwrap().unwrap().memory_id;
709 assert!(!bound_a.is_empty(), "first same-named agent must be backfilled");
710 assert!(!bound_b.is_empty(), "second same-named agent must NOT be left unbound by a collision");
711 assert_ne!(bound_a, bound_b, "each agent still gets its own distinct bundle");
712 });
713 }
714
715 #[test]
716 fn missing_channel_store_is_a_noop() {
717 with_isolated_home(|_home| {
718 let shared_tmp = tempfile::NamedTempFile::new().unwrap();
719 let ctx = ctx_for(std::path::Path::new("Z:/does/not/exist/objects.db"), shared_tmp.path());
720 M0021BackfillAgentBundles.up(&ctx).unwrap();
721 });
722 }
723
724 #[test]
730 fn falls_back_to_the_channel_store_when_the_shared_store_path_is_unusable() {
731 with_isolated_home(|_home| {
732 let tmp = tempfile::NamedTempFile::new().unwrap();
733 let mstore = Store::open(tmp.path()).unwrap();
734 let id = insert_def(&mstore, "Fallback Agent");
735 let bad_shared_path = std::path::Path::new("Z:/does/not/exist/shared-store.db");
736
737 M0021BackfillAgentBundles.up(&ctx_for(tmp.path(), bad_shared_path)).unwrap();
738
739 let def = mstore.agent_def_get(&id).unwrap().unwrap();
740 assert!(!def.memory_id.is_empty(), "must still backfill even when the shared store is unreachable");
741 assert!(mstore.bundle_get(&def.memory_id).unwrap().is_some(), "falls back to writing the bundle in the channel store");
742 });
743 }
744}