1use std::sync::Arc;
38
39use serde::Deserialize;
40
41use super::storage::bundles::Bundle;
42use super::storage::store::Store;
43use super::storage::{BundleReseedOutcome, StoreError};
44
45pub const WRITTEN_BY: &str = "agentmux-operator-config-seed";
53
54const SEED_MANIFEST: &str = include_str!("../../operator-config-seed.json");
55
56#[derive(Debug, Deserialize)]
57struct SeedManifest {
58 version: u32,
66 entries: Vec<SeedEntry>,
67}
68
69#[derive(Debug, Deserialize)]
70struct SeedEntry {
71 id: String,
72 name: String,
73 instructions: String,
74 #[serde(default)]
82 agent_types: Vec<String>,
83}
84
85pub fn agent_kind(agent_mode: &str) -> &'static str {
90 if agent_mode == "container" { "container" } else { "host" }
91}
92
93pub fn agent_mode_of_block(mstore: &Store, block_id: &str) -> String {
98 match mstore.get::<crate::backend::obj::Block>(block_id) {
99 Ok(Some(block)) => crate::backend::obj::meta_get_string(&block.meta, "agentMode", "host"),
100 Ok(None) => "host".to_string(),
101 Err(e) => {
102 tracing::warn!(block_id, error = %e, "operator config: block lookup failed — delivering as a host agent");
103 "host".to_string()
104 }
105 }
106}
107
108pub fn agent_kind_of_block(mstore: &Store, block_id: &str) -> Option<&'static str> {
112 let block = mstore.get::<crate::backend::obj::Block>(block_id).ok()??;
113 Some(agent_kind(&crate::backend::obj::meta_get_string(&block.meta, "agentMode", "host")))
114}
115
116fn embedded_manifest() -> Option<&'static SeedManifest> {
119 static MANIFEST: std::sync::OnceLock<Option<SeedManifest>> = std::sync::OnceLock::new();
120 MANIFEST.get_or_init(|| serde_json::from_str(SEED_MANIFEST).ok()).as_ref()
121}
122
123pub fn global_bundles_for_agent(bundles: Vec<Bundle>, agent_mode: &str) -> Vec<Bundle> {
132 match embedded_manifest() {
133 Some(manifest) => filter_for_kind(bundles, manifest, agent_kind(agent_mode)),
134 None => bundles,
135 }
136}
137
138fn filter_for_kind(bundles: Vec<Bundle>, manifest: &SeedManifest, kind: &str) -> Vec<Bundle> {
139 bundles
140 .into_iter()
141 .filter(|b| {
142 !b.is_system
143 || manifest
144 .entries
145 .iter()
146 .find(|e| e.id == b.id)
147 .is_none_or(|e| e.agent_types.is_empty() || e.agent_types.iter().any(|t| t == kind))
148 })
149 .collect()
150}
151
152pub fn auto_seed_on_startup(store: &Arc<Store>) {
156 let manifest: SeedManifest = match serde_json::from_str(SEED_MANIFEST) {
157 Ok(m) => m,
158 Err(e) => {
159 tracing::error!("operator config seed: failed to parse manifest: {e}");
160 return;
161 }
162 };
163 seed_from_manifest(store, &manifest);
164}
165
166fn seed_from_manifest(store: &Arc<Store>, manifest: &SeedManifest) {
170 prune_entries_removed_from_manifest(store, manifest);
177
178 let mut tally = SeedTally::default();
179
180 let mut pending: Vec<&SeedEntry> = manifest.entries.iter().collect();
200 loop {
201 let mut still_pending = Vec::new();
202 let mut made_progress = false;
203 for entry in pending {
204 if seed_and_tally(store, entry, manifest.version, &mut tally, false) {
205 made_progress = true;
206 } else {
207 still_pending.push(entry);
208 }
209 }
210 if still_pending.is_empty() {
211 break;
212 }
213 if !made_progress {
214 for entry in still_pending {
218 seed_and_tally(store, entry, manifest.version, &mut tally, true);
219 }
220 break;
221 }
222 pending = still_pending;
223 }
224
225 tally.log(manifest.version);
226}
227
228#[derive(Default)]
230struct SeedTally {
231 created: usize,
232 updated: usize,
233 unchanged: usize,
234 skipped_local: usize,
235 skipped_older: usize,
236 skipped_collision: usize,
237}
238
239impl SeedTally {
240 fn log(&self, manifest_version: u32) {
241 let Self { created, updated, unchanged, skipped_local, skipped_older, skipped_collision } = *self;
242 if created + updated + skipped_local + skipped_older + skipped_collision > 0 {
243 tracing::info!(
244 "operator config seed: manifest v{manifest_version}: {created} created, {updated} updated, \
245 {unchanged} unchanged, {skipped_local} skipped (local edit), \
246 {skipped_older} skipped (older generation), {skipped_collision} skipped (collision)",
247 );
248 }
249 }
250}
251
252fn seed_and_tally(store: &Arc<Store>, entry: &SeedEntry, manifest_version: u32, tally: &mut SeedTally, is_retry: bool) -> bool {
259 match seed_one(store, entry, manifest_version) {
260 Ok(BundleReseedOutcome::Created) => { tally.created += 1; true }
261 Ok(BundleReseedOutcome::Updated) => { tally.updated += 1; true }
262 Ok(BundleReseedOutcome::Unchanged) => { tally.unchanged += 1; true }
263 Ok(BundleReseedOutcome::SkippedLocalEdit) => {
264 tally.skipped_local += 1;
265 tracing::info!(
266 id = %entry.id,
267 "operator config seed: skipping — entry has a local (non-AgentMux) edit, not overwriting"
268 );
269 true
270 }
271 Ok(BundleReseedOutcome::SkippedOlderGeneration) => {
272 tally.skipped_older += 1;
273 tracing::info!(
274 id = %entry.id,
275 manifest_version,
276 "operator config seed: skipping — a newer manifest generation already seeded this row \
277 (this build's own manifest is older; likely a different AgentMux version sharing this store)"
278 );
279 true
280 }
281 Ok(BundleReseedOutcome::SkippedRetired) => {
282 tally.skipped_older += 1;
283 tracing::info!(
284 id = %entry.id,
285 manifest_version,
286 "operator config seed: skipping — a newer manifest generation already retired this id \
287 (this build's own manifest still lists it; not resurrecting it)"
288 );
289 true
290 }
291 Err(e) => {
292 if is_retry {
293 tally.skipped_collision += 1;
294 tracing::warn!(
295 id = %entry.id,
296 name = %entry.name,
297 error = %e,
298 "operator config seed: skipping entry due to upsert error (name collision?)"
299 );
300 }
301 false
302 }
303 }
304}
305
306fn prune_entries_removed_from_manifest(store: &Arc<Store>, manifest: &SeedManifest) {
322 let manifest_ids: std::collections::HashSet<&str> =
323 manifest.entries.iter().map(|e| e.id.as_str()).collect();
324
325 let existing_ids = match store.bundle_list_system_ids() {
326 Ok(ids) => ids,
327 Err(e) => {
328 tracing::error!("operator config seed: failed to list existing system entries for pruning: {e}");
329 return;
330 }
331 };
332
333 let mut pruned = 0usize;
334 for id in existing_ids {
335 if manifest_ids.contains(id.as_str()) {
336 continue;
337 }
338 match store.bundle_delete_system_if_owned(&id, WRITTEN_BY, manifest.version) {
339 Ok(true) => {
340 pruned += 1;
341 tracing::info!(id = %id, "operator config seed: pruned an entry no longer in the manifest");
342 }
343 Ok(false) => {
344 tracing::info!(
345 id = %id,
346 "operator config seed: an entry is no longer in this manifest but was left in place \
347 (either it has a local edit, or its own generation is newer than this manifest's — \
348 likely a different, newer AgentMux build sharing this store)"
349 );
350 }
351 Err(e) => {
352 tracing::warn!(id = %id, error = %e, "operator config seed: failed to prune an entry no longer in the manifest");
353 }
354 }
355 }
356 if pruned > 0 {
357 tracing::info!("operator config seed: pruned {pruned} entries no longer in the manifest");
358 }
359}
360
361fn seed_one(store: &Arc<Store>, entry: &SeedEntry, manifest_version: u32) -> Result<BundleReseedOutcome, StoreError> {
362 let now = agentmux_common::time::now_ms();
363 let bundle = Bundle {
364 id: entry.id.clone(),
365 name: entry.name.clone(),
366 description: String::new(),
367 is_blank: false,
368 is_global: true,
369 provider: String::new(),
370 model: String::new(),
371 instructions: entry.instructions.clone(),
372 instructions_by_provider: "{}".to_string(),
373 context_files: "[]".to_string(),
374 mcp_servers: "[]".to_string(),
375 skills: "[]".to_string(),
376 sort_order: 0,
377 created_at: now,
378 updated_at: now,
379 is_system: true,
380 };
381
382 let source_detail = format!(r#"{{"manifest_version":{manifest_version}}}"#);
387 store.bundle_reseed_system_if_owned(&bundle, WRITTEN_BY, "agentmux_operator_config_seed", &source_detail)
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 fn test_store() -> Arc<Store> {
395 Arc::new(Store::open_in_memory().unwrap())
396 }
397
398 fn entry(id: &str, name: &str, instructions: &str) -> SeedEntry {
399 SeedEntry {
400 id: id.to_string(),
401 name: name.to_string(),
402 instructions: instructions.to_string(),
403 agent_types: Vec::new(),
404 }
405 }
406
407 #[test]
408 fn embedded_manifest_parses() {
409 let manifest: SeedManifest = serde_json::from_str(SEED_MANIFEST).expect("valid JSON");
410 assert!(!manifest.entries.is_empty());
411 for e in &manifest.entries {
412 assert!(!e.id.is_empty());
413 assert!(!e.name.is_empty());
414 assert!(!e.instructions.trim().is_empty());
415 }
416 }
417
418 #[test]
419 fn fresh_install_creates_every_entry() {
420 let store = test_store();
421 let e = entry("op-1", "Operator One", "body one");
422 let outcome = seed_one(&store, &e, 1).unwrap();
423 assert!(matches!(outcome, BundleReseedOutcome::Created));
424
425 let bundle = store.bundle_get("op-1").unwrap().expect("row exists");
426 assert!(bundle.is_system);
427 assert!(bundle.is_global);
428 assert_eq!(bundle.instructions, "body one");
429
430 let history = store.bundle_version_list("op-1").unwrap();
431 assert_eq!(history.len(), 1);
432 assert_eq!(history[0].written_by, WRITTEN_BY);
433 }
434
435 #[test]
436 fn unchanged_manifest_is_a_no_op() {
437 let store = test_store();
438 let e = entry("op-1", "Operator One", "body one");
439 seed_one(&store, &e, 1).unwrap();
440
441 let outcome = seed_one(&store, &e, 1).unwrap();
442 assert!(matches!(outcome, BundleReseedOutcome::Unchanged));
443 assert_eq!(store.bundle_version_list("op-1").unwrap().len(), 1, "no new version on no-op");
444 }
445
446 #[test]
454 fn unchanged_content_still_advances_the_recorded_generation() {
455 let store = test_store();
456 seed_one(&store, &entry("op-1", "Operator One", "the text"), 1).unwrap();
457
458 let outcome = seed_one(&store, &entry("op-1", "Operator One", "the text"), 3).unwrap();
461 assert!(matches!(outcome, BundleReseedOutcome::Unchanged), "content itself really is unchanged");
462 assert_eq!(store.bundle_version_list("op-1").unwrap().len(), 2, "the generation bump must still be recorded as a version");
463
464 let outcome = seed_one(&store, &entry("op-1", "Operator One", "generation 2's own different text"), 2).unwrap();
468 assert!(matches!(outcome, BundleReseedOutcome::SkippedOlderGeneration));
469 assert_eq!(store.bundle_get("op-1").unwrap().unwrap().instructions, "the text");
470 }
471
472 #[test]
473 fn changed_manifest_content_overwrites_when_seeder_owns_it() {
474 let store = test_store();
475 let e1 = entry("op-1", "Operator One", "body one");
476 seed_one(&store, &e1, 1).unwrap();
477
478 let e2 = entry("op-1", "Operator One", "body TWO");
479 let outcome = seed_one(&store, &e2, 2).unwrap();
480 assert!(matches!(outcome, BundleReseedOutcome::Updated));
481
482 let bundle = store.bundle_get("op-1").unwrap().unwrap();
483 assert_eq!(bundle.instructions, "body TWO");
484 assert_eq!(store.bundle_version_list("op-1").unwrap().len(), 2);
485 }
486
487 #[test]
496 fn unrecorded_edit_from_an_unversioned_writer_is_never_overwritten() {
497 let store = test_store();
498 seed_one(&store, &entry("op-1", "Operator One", "seeded body"), 1).unwrap();
499
500 let mut edited = store.bundle_get("op-1").unwrap().unwrap();
503 edited.instructions = "edited by an older, unversioned build".to_string();
504 store.bundle_upsert_system(&edited).unwrap();
505
506 let history = store.bundle_version_list("op-1").unwrap();
509 assert_eq!(history.len(), 1, "the unversioned write left no trace in the version table");
510 assert_eq!(history[0].written_by, WRITTEN_BY);
511
512 let outcome = seed_one(&store, &entry("op-1", "Operator One", "a real manifest update"), 2).unwrap();
513 assert!(matches!(outcome, BundleReseedOutcome::SkippedLocalEdit));
514
515 let bundle = store.bundle_get("op-1").unwrap().unwrap();
516 assert_eq!(bundle.instructions, "edited by an older, unversioned build", "the unrecorded edit must survive");
517 }
518
519 #[test]
527 fn row_with_zero_version_history_is_never_overwritten() {
528 let store = test_store();
529 let bundle = Bundle {
532 id: "op-1".to_string(),
533 name: "Operator One".to_string(),
534 description: String::new(),
535 is_blank: false,
536 is_global: true,
537 provider: String::new(),
538 model: String::new(),
539 instructions: "content with no version history".to_string(),
540 instructions_by_provider: "{}".to_string(),
541 context_files: "[]".to_string(),
542 mcp_servers: "[]".to_string(),
543 skills: "[]".to_string(),
544 sort_order: 0,
545 created_at: 0,
546 updated_at: 0,
547 is_system: true,
548 };
549 store.bundle_upsert_system(&bundle).unwrap();
550 assert!(store.bundle_version_list("op-1").unwrap().is_empty(), "precondition: no version history at all");
551
552 let outcome = seed_one(&store, &entry("op-1", "Operator One", "a manifest update"), 1).unwrap();
553 assert!(matches!(outcome, BundleReseedOutcome::SkippedLocalEdit));
554 assert_eq!(store.bundle_get("op-1").unwrap().unwrap().instructions, "content with no version history");
555 }
556
557 #[test]
563 fn older_manifest_generation_never_downgrades_a_newer_seed() {
564 let store = test_store();
565 let e_v3 = entry("op-1", "Operator One", "content from generation 3");
566 seed_one(&store, &e_v3, 3).unwrap();
567
568 let e_v1 = entry("op-1", "Operator One", "content from generation 1");
571 let outcome = seed_one(&store, &e_v1, 1).unwrap();
572 assert!(matches!(outcome, BundleReseedOutcome::SkippedOlderGeneration));
573
574 let bundle = store.bundle_get("op-1").unwrap().unwrap();
575 assert_eq!(bundle.instructions, "content from generation 3", "newer generation's content must survive");
576 }
577
578 #[test]
589 fn locally_edited_entry_is_never_overwritten() {
590 let store = test_store();
591 let e1 = entry("op-1", "Operator One", "body one");
592 seed_one(&store, &e1, 1).unwrap();
593
594 let edited = Bundle {
597 id: "op-1".to_string(),
598 name: "Operator One".to_string(),
599 description: String::new(),
600 is_blank: false,
601 is_global: true,
602 provider: String::new(),
603 model: String::new(),
604 instructions: "human-edited body".to_string(),
605 instructions_by_provider: "{}".to_string(),
606 context_files: "[]".to_string(),
607 mcp_servers: "[]".to_string(),
608 skills: "[]".to_string(),
609 sort_order: 0,
610 created_at: 0,
611 updated_at: 0,
612 is_system: true,
613 };
614 store.bundle_upsert_system_with_version(&edited, "armory-ui", "human", "{}").unwrap();
615
616 let e2 = entry("op-1", "Operator One", "AgentMux wants to change this");
617 let outcome = seed_one(&store, &e2, 2).unwrap();
618 assert!(matches!(outcome, BundleReseedOutcome::SkippedLocalEdit));
619
620 let bundle = store.bundle_get("op-1").unwrap().unwrap();
621 assert_eq!(bundle.instructions, "human-edited body", "seed_one made no db_bundles write on skip");
622 }
623
624 #[test]
628 fn seeder_owned_entry_removed_from_manifest_is_pruned() {
629 let store = test_store();
630 seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
631 assert!(store.bundle_get("op-old").unwrap().is_some());
632
633 let manifest = SeedManifest { version: 2, entries: vec![entry("op-new", "New Entry", "new content")] };
635 seed_one(&store, &manifest.entries[0], manifest.version).unwrap();
638 prune_entries_removed_from_manifest(&store, &manifest);
639
640 assert!(store.bundle_get("op-old").unwrap().is_none(), "seeder-owned, manifest-removed entry must be pruned");
641 assert!(store.bundle_get("op-new").unwrap().is_some(), "entries still in the manifest are untouched");
642 }
643
644 #[test]
652 fn prune_does_not_delete_an_unrecorded_edit_from_an_unversioned_writer() {
653 let store = test_store();
654 seed_one(&store, &entry("op-old", "Old Entry", "seeded content"), 1).unwrap();
655
656 let mut edited = store.bundle_get("op-old").unwrap().unwrap();
658 edited.instructions = "edited by an older, unversioned build".to_string();
659 store.bundle_upsert_system(&edited).unwrap();
660
661 let manifest = SeedManifest { version: 2, entries: vec![] };
664 prune_entries_removed_from_manifest(&store, &manifest);
665
666 let bundle = store.bundle_get("op-old").unwrap();
667 assert!(bundle.is_some(), "an unrecorded edit must never be deleted");
668 assert_eq!(bundle.unwrap().instructions, "edited by an older, unversioned build");
669 }
670
671 #[test]
675 fn locally_edited_entry_removed_from_manifest_is_not_pruned() {
676 let store = test_store();
677 seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
678
679 let edited = {
680 let mut b = store.bundle_get("op-old").unwrap().unwrap();
681 b.instructions = "a human edit worth keeping".to_string();
682 b
683 };
684 store.bundle_upsert_system_with_version(&edited, "armory-ui", "human", "{}").unwrap();
685
686 let manifest = SeedManifest { version: 2, entries: vec![] };
687 prune_entries_removed_from_manifest(&store, &manifest);
688
689 let bundle = store.bundle_get("op-old").unwrap();
690 assert!(bundle.is_some(), "a human-edited entry must survive even after its manifest entry is removed");
691 assert_eq!(bundle.unwrap().instructions, "a human edit worth keeping");
692 }
693
694 #[test]
700 fn seed_one_does_not_resurrect_an_entry_retired_by_a_newer_generation() {
701 let store = test_store();
702 seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
703
704 assert!(store.bundle_delete_system_if_owned("op-old", WRITTEN_BY, 5).unwrap());
707 assert!(store.bundle_get("op-old").unwrap().is_none());
708
709 let outcome = seed_one(&store, &entry("op-old", "Old Entry", "old content"), 3).unwrap();
712 assert!(matches!(outcome, BundleReseedOutcome::SkippedRetired));
713 assert!(store.bundle_get("op-old").unwrap().is_none(), "a retired entry must not be resurrected by an older manifest");
714 }
715
716 #[test]
722 fn seed_one_recreates_a_retired_entry_once_caller_generation_catches_up() {
723 let store = test_store();
724 seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
725 assert!(store.bundle_delete_system_if_owned("op-old", WRITTEN_BY, 5).unwrap());
726
727 let outcome = seed_one(&store, &entry("op-old", "Reintroduced Entry", "new content"), 5).unwrap();
728 assert!(matches!(outcome, BundleReseedOutcome::Created));
729 assert_eq!(store.bundle_get("op-old").unwrap().unwrap().instructions, "new content");
730 }
731
732 #[test]
739 fn seed_one_still_recreates_after_an_ordinary_human_delete_no_tombstone() {
740 let store = test_store();
741 seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
742 assert!(store.bundle_delete_system("op-old").unwrap());
743
744 let outcome = seed_one(&store, &entry("op-old", "Old Entry", "old content"), 1).unwrap();
745 assert!(matches!(outcome, BundleReseedOutcome::Created));
746 }
747
748 #[test]
755 fn prune_does_not_delete_an_entry_from_a_newer_generation_it_does_not_know_about() {
756 let store = test_store();
757 seed_one(&store, &entry("op-new", "New Entry", "added in gen 5"), 5).unwrap();
760
761 let older_manifest = SeedManifest { version: 3, entries: vec![] };
764 prune_entries_removed_from_manifest(&store, &older_manifest);
765
766 assert!(
767 store.bundle_get("op-new").unwrap().is_some(),
768 "an older build must not delete a newer build's own addition"
769 );
770 }
771
772 #[test]
777 fn prune_still_deletes_once_this_process_generation_has_caught_up() {
778 let store = test_store();
779 seed_one(&store, &entry("op-old", "Old Entry", "added in gen 1"), 1).unwrap();
780
781 let later_manifest = SeedManifest { version: 3, entries: vec![] };
782 prune_entries_removed_from_manifest(&store, &later_manifest);
783
784 assert!(store.bundle_get("op-old").unwrap().is_none(), "a genuinely-removed entry must still be pruned");
785 }
786
787 #[test]
792 fn deleted_entry_is_recreated_even_when_manifest_content_is_unchanged() {
793 let store = test_store();
794 let e = entry("op-1", "Operator One", "body one");
795 seed_one(&store, &e, 1).unwrap();
796 assert!(store.bundle_delete_system("op-1").unwrap(), "row deleted");
797 assert!(store.bundle_get("op-1").unwrap().is_none());
798 assert_eq!(store.bundle_version_list("op-1").unwrap().len(), 1, "history survives the delete");
799
800 let outcome = seed_one(&store, &e, 1).unwrap();
801 assert!(matches!(outcome, BundleReseedOutcome::Created), "row existence, not history, gates recreation");
802 assert!(store.bundle_get("op-1").unwrap().is_some());
803 }
804
805 #[test]
806 fn name_collision_with_existing_ordinary_bundle_is_skipped_not_fatal() {
807 let store = test_store();
808 let existing = Bundle {
810 id: "user-bundle-1".to_string(),
811 name: "Operator One".to_string(),
812 description: String::new(),
813 is_blank: false,
814 is_global: false,
815 provider: String::new(),
816 model: String::new(),
817 instructions: "unrelated user content".to_string(),
818 instructions_by_provider: "{}".to_string(),
819 context_files: "[]".to_string(),
820 mcp_servers: "[]".to_string(),
821 skills: "[]".to_string(),
822 sort_order: 0,
823 created_at: 0,
824 updated_at: 0,
825 is_system: false,
826 };
827 store.bundle_upsert(&existing).unwrap();
828
829 let e = entry("op-1", "Operator One", "body one");
830 let result = seed_one(&store, &e, 1);
831 assert!(result.is_err(), "name UNIQUE constraint should surface as an error, not silently create op-1");
832 assert!(store.bundle_get("op-1").unwrap().is_none());
833 }
834
835 #[test]
842 fn seeding_succeeds_regardless_of_entry_order_within_one_manifest() {
843 let store = test_store();
844 seed_one(&store, &entry("op-a", "Name X", "original A content"), 1).unwrap();
845
846 let manifest = SeedManifest {
850 version: 2,
851 entries: vec![
852 entry("op-b", "Name X", "B content"),
853 entry("op-a", "Name Y", "updated A content"),
854 ],
855 };
856 seed_from_manifest(&store, &manifest);
857
858 let a = store.bundle_get("op-a").unwrap().expect("A must still exist");
859 assert_eq!(a.name, "Name Y");
860 assert_eq!(a.instructions, "updated A content");
861
862 let b = store.bundle_get("op-b").unwrap().expect("B must exist despite the first-pass ordering collision");
863 assert_eq!(b.name, "Name X");
864 assert_eq!(b.instructions, "B content");
865 }
866
867 #[test]
873 fn seeding_resolves_a_three_entry_name_dependency_chain() {
874 let store = test_store();
875 seed_one(&store, &entry("op-a", "Name A", "original A"), 1).unwrap();
876 seed_one(&store, &entry("op-b", "Name B", "original B"), 1).unwrap();
877
878 let manifest = SeedManifest {
881 version: 2,
882 entries: vec![
883 entry("op-c", "Name B", "C content"),
884 entry("op-b", "Name A", "updated B"),
885 entry("op-a", "Name C", "updated A"),
886 ],
887 };
888 seed_from_manifest(&store, &manifest);
889
890 assert_eq!(store.bundle_get("op-a").unwrap().unwrap().name, "Name C");
891 assert_eq!(store.bundle_get("op-b").unwrap().unwrap().name, "Name A");
892 let c = store.bundle_get("op-c").unwrap().expect("C must exist despite being at the end of the dependency chain");
893 assert_eq!(c.name, "Name B");
894 assert_eq!(c.instructions, "C content");
895 }
896
897 #[test]
898 fn auto_seed_on_startup_does_not_panic_on_real_manifest() {
899 let store = test_store();
902 auto_seed_on_startup(&store);
903 let manifest: SeedManifest = serde_json::from_str(SEED_MANIFEST).unwrap();
904 for e in &manifest.entries {
905 let bundle = store.bundle_get(&e.id).unwrap().expect("seeded");
906 assert!(bundle.is_system);
907 }
908 }
909
910 fn global(id: &str, is_system: bool) -> Bundle {
911 Bundle {
912 id: id.to_string(),
913 name: id.to_string(),
914 description: String::new(),
915 is_blank: false,
916 is_global: true,
917 provider: String::new(),
918 model: String::new(),
919 instructions: format!("{id} body"),
920 instructions_by_provider: "{}".to_string(),
921 context_files: "[]".to_string(),
922 mcp_servers: "[]".to_string(),
923 skills: "[]".to_string(),
924 sort_order: 0,
925 created_at: 0,
926 updated_at: 0,
927 is_system,
928 }
929 }
930
931 fn ids(bundles: &[Bundle]) -> Vec<&str> {
932 bundles.iter().map(|b| b.id.as_str()).collect()
933 }
934
935 #[test]
936 fn agent_kind_is_container_only_for_container() {
937 assert_eq!(agent_kind("container"), "container");
938 for mode in ["host", "standalone", "", "anything"] {
939 assert_eq!(agent_kind(mode), "host", "{mode:?}");
940 }
941 }
942
943 #[test]
944 fn targeted_system_entry_reaches_only_its_kind() {
945 let mut host_only = entry("op-host", "Host", "h");
946 host_only.agent_types = vec!["host".to_string()];
947 let manifest = SeedManifest { version: 1, entries: vec![entry("op-all", "All", "a"), host_only] };
948 let bundles = || vec![global("op-all", true), global("op-host", true), global("ws", false)];
949
950 assert_eq!(ids(&filter_for_kind(bundles(), &manifest, "host")), ["op-all", "op-host", "ws"]);
951 assert_eq!(ids(&filter_for_kind(bundles(), &manifest, "container")), ["op-all", "ws"]);
952 }
953
954 #[test]
955 fn unknown_system_entries_and_workspace_entries_always_stay() {
956 let manifest = SeedManifest { version: 1, entries: vec![] };
959 let kept = filter_for_kind(vec![global("op-newer", true), global("ws", false)], &manifest, "container");
960 assert_eq!(ids(&kept), ["op-newer", "ws"]);
961 }
962
963 #[test]
964 fn workspace_entry_is_host_only_in_the_real_manifest() {
965 let workspace = global("operator-config-workspace", true);
966 let app_api = global("operator-config-app-api", true);
967 let host = global_bundles_for_agent(vec![app_api.clone(), workspace.clone()], "standalone");
968 assert_eq!(ids(&host), ["operator-config-app-api", "operator-config-workspace"]);
969 let container = global_bundles_for_agent(vec![app_api, workspace], "container");
970 assert_eq!(ids(&container), ["operator-config-app-api"]);
971 }
972}
973
974#[cfg(test)]
975mod swarm_broadcast_entry_tests {
976 use super::*;
977
978 fn shipped() -> SeedManifest {
979 serde_json::from_str(SEED_MANIFEST).expect("valid JSON")
980 }
981
982 #[test]
983 fn the_swarm_broadcast_entry_ships_to_every_kind_of_agent() {
984 let manifest = shipped();
985 let entry = manifest.entries.iter().find(|e| e.id == "operator-config-swarm-broadcast").expect("entry present");
986 assert!(entry.agent_types.is_empty(), "no kind filter: container agents receive broadcasts too");
987 }
988
989 #[test]
990 fn it_tells_an_agent_to_follow_a_broadcast_without_stopping_to_ask() {
991 let manifest = shipped();
992 let entry = manifest.entries.iter().find(|e| e.id == "operator-config-swarm-broadcast").unwrap();
993 let text = &entry.instructions;
994 assert!(text.contains("[BROADCAST:FROM=user VIA=swarm"), "names the exact header");
995 assert!(text.contains("do not stop to ask for confirmation"));
996 assert!(text.contains("It is not a jekt"));
997 assert!(text.contains("decided by AgentMux, not by this line"));
1000 }
1001
1002 #[test]
1003 fn the_header_the_entry_describes_is_the_one_srv_writes() {
1004 let header = crate::backend::reactive::broadcast_turn_message("agent1", 3, "m1", "x");
1006 let first = header.lines().next().unwrap();
1007 assert!(first.starts_with("[BROADCAST:FROM=user VIA=swarm TO=agent1 RECIPIENTS=3 MSGID=m1 TS="), "{first}");
1008 let manifest = shipped();
1009 let entry = manifest.entries.iter().find(|e| e.id == "operator-config-swarm-broadcast").unwrap();
1010 for field in ["FROM=user", "VIA=swarm", "TO=", "RECIPIENTS=", "MSGID=", "TS="] {
1011 assert!(entry.instructions.contains(field), "entry omits {field}");
1012 }
1013 }
1014
1015 #[test]
1016 fn the_manifest_version_moved_with_the_new_entry() {
1017 assert!(shipped().version >= 8);
1018 }
1019
1020 #[test]
1021 fn the_system_notes_it_describes_are_the_ones_srv_writes() {
1022 use crate::muxbus::delivery_status::notes;
1023 let manifest = shipped();
1024 let entry = manifest.entries.iter().find(|e| e.id == "operator-config-swarm-broadcast").unwrap();
1025 let text = &entry.instructions;
1026 assert!(text.contains(&format!("`{} ", notes::NOTE_MARKER)), "names the marker");
1027 assert!(text.contains("a jekt cannot carry one"));
1028 let pause = notes::pause_note(0, "MuxBus needs a sign-in");
1030 let after_time = &pause[pause.find(" (").unwrap()..];
1031 assert!(text.contains(after_time), "pause note drifted: {after_time}");
1032 assert!(text.contains("Cloud messages are paused since "));
1033 assert!(text.contains("Cloud messages resumed at "));
1034 assert!(notes::delayed_prefix(Some(0), notes::DELAYED_AFTER_MS + 1).starts_with("(sent "));
1035 assert!(text.contains("(sent 14:30, delivered 14:41)"));
1036 assert!(manifest.version >= 12);
1037 }
1038}