agentmux_srv/migrations/
m0016_seed_starter_mcp_servers.rs1use std::sync::Arc;
26
27use crate::backend::mcp_seed::{any_starter_mcp_server_name_exists, seed_starter_mcp_servers};
28use crate::backend::storage::store::Store;
29use super::{Migration, MigrationContext, MigrationError, MigrationScope};
30
31pub struct M0016SeedStarterMcpServers;
32
33impl Migration for M0016SeedStarterMcpServers {
34 fn id(&self) -> &'static str { "0016_seed_starter_mcp_servers" }
35 fn scope(&self) -> MigrationScope { MigrationScope::Channel }
36 fn description(&self) -> &'static str { "Seed the global MCP Servers catalog with a curated starter set" }
37
38 fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError> {
39 if !ctx.channel_store_path.exists() {
40 return Ok(());
41 }
42 let mstore = Arc::new(
43 Store::open(&ctx.channel_store_path)
44 .map_err(|e| MigrationError(format!("seed_starter_mcp_servers: open mstore: {}", e)))?,
45 );
46 if any_starter_mcp_server_name_exists(&mstore)
47 .map_err(|e| MigrationError(format!("seed_starter_mcp_servers: check existing: {}", e)))?
48 {
49 return Ok(());
50 }
51 seed_starter_mcp_servers(&mstore)
52 .map(|_| ())
53 .map_err(|e| MigrationError(format!("seed_starter_mcp_servers: {}", e)))
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 fn ctx_for(path: &std::path::Path) -> MigrationContext {
62 MigrationContext {
63 home: std::env::temp_dir(),
64 data_dir: std::env::temp_dir(),
65 shared_store_path: std::env::temp_dir().join("unused-store.db"),
66 channel_store_path: path.to_path_buf(),
67 }
68 }
69
70 #[test]
71 fn seeds_six_starter_mcp_servers_on_a_fresh_channel() {
72 let tmp = tempfile::NamedTempFile::new().unwrap();
73 Store::open(tmp.path()).unwrap();
76
77 M0016SeedStarterMcpServers.up(&ctx_for(tmp.path())).unwrap();
78
79 let mstore = Store::open(tmp.path()).unwrap();
80 assert_eq!(mstore.mcp_server_list_global(&mstore).unwrap().len(), 6);
81 }
82
83 #[test]
84 fn once_marked_applied_a_full_catalog_deletion_is_never_resurrected() {
85 let tmp = tempfile::NamedTempFile::new().unwrap();
89 let mstore = Store::open(tmp.path()).unwrap();
90 let ctx = ctx_for(tmp.path());
91
92 M0016SeedStarterMcpServers.up(&ctx).unwrap();
93 mstore.migration_mark_applied("0016_seed_starter_mcp_servers", "channel", 0).unwrap();
94 assert_eq!(mstore.mcp_server_list_global(&mstore).unwrap().len(), 6);
95
96 for item in mstore.mcp_server_list_global(&mstore).unwrap() {
97 mstore.mcp_server_delete(&mstore, &item.server.id).unwrap();
98 }
99 assert!(mstore.mcp_server_list_global(&mstore).unwrap().is_empty());
100
101 assert!(
105 mstore.migration_is_applied("0016_seed_starter_mcp_servers"),
106 "once applied, the tracking row must persist regardless of catalog contents"
107 );
108 }
109
110 #[test]
111 fn self_heals_when_starter_servers_already_exist_from_a_prior_run() {
112 let tmp = tempfile::NamedTempFile::new().unwrap();
118 let mstore = Store::open(tmp.path()).unwrap();
119 let ctx = ctx_for(tmp.path());
120
121 let pre_existing = crate::backend::storage::mcp_servers::McpServer {
122 id: "pre-existing-from-old-path".to_string(),
123 name: "git".to_string(),
124 transport: "stdio".to_string(),
125 config: "{}".to_string(),
126 is_global: true,
127 created_at: 0,
128 updated_at: 0,
129 };
130 mstore.mcp_server_upsert_unique_global(&pre_existing).unwrap();
131
132 M0016SeedStarterMcpServers.up(&ctx).unwrap();
133
134 let after = mstore.mcp_server_list_global(&mstore).unwrap();
135 assert_eq!(
136 after.len(),
137 1,
138 "up() must skip seeding entirely on a name collision, not insert the other 5"
139 );
140 assert_eq!(after[0].server.id, "pre-existing-from-old-path");
141 }
142}