agentmux_srv/migrations/
m0019_repair_malformed_secret_ref.rs1use crate::backend::storage::store::Store;
27use super::{Migration, MigrationContext, MigrationError, MigrationScope};
28
29pub struct M0019RepairMalformedSecretRef;
30
31impl Migration for M0019RepairMalformedSecretRef {
32 fn id(&self) -> &'static str { "0019_repair_malformed_secret_ref" }
33 fn scope(&self) -> MigrationScope { MigrationScope::Global }
34 fn description(&self) -> &'static str {
35 "Repair db_accounts.secret_ref rows with unescaped-backslash JSON corruption"
36 }
37
38 fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError> {
39 if !ctx.shared_store_path.exists() {
40 return Ok(());
41 }
42 let shared = Store::open_shared(&ctx.shared_store_path)
43 .map_err(|e| MigrationError(format!("repair_malformed_secret_ref: open shared store: {}", e)))?;
44 let repaired = shared
45 .identity_repair_malformed_secret_refs()
46 .map_err(|e| MigrationError(format!("repair_malformed_secret_ref: {}", e)))?;
47 tracing::info!(
48 target: "identity",
49 repaired,
50 "identity.repair: secret_ref repair pass — {} row(s) fixed",
51 repaired,
52 );
53 Ok(())
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::backend::storage::store::{IdentityAccount, SecretRef};
61
62 fn ctx_for(shared: &std::path::Path) -> MigrationContext {
63 MigrationContext {
64 home: std::env::temp_dir(),
65 data_dir: std::env::temp_dir(),
66 shared_store_path: shared.to_path_buf(),
67 channel_store_path: std::env::temp_dir().join("agentmux-test-unused-channel.db"),
68 }
69 }
70
71 #[test]
72 fn repairs_a_malformed_row_in_the_shared_store() {
73 let shared = tempfile::NamedTempFile::new().unwrap();
74 let store = Store::open_shared(shared.path()).unwrap();
75 store
76 .identity_upsert(&IdentityAccount {
77 id: "acct-good".to_string(),
78 name: "claude-good".to_string(),
79 provider: "claude".to_string(),
80 kind: "oauth".to_string(),
81 display_name: String::new(),
82 secret_ref: SecretRef::OAuthConfigDir { dir: "/tmp/good".to_string() },
83 context: serde_json::json!({}),
84 status: "ok".to_string(),
85 created_at: 0,
86 updated_at: 0,
87 })
88 .unwrap();
89 {
90 let conn = store.conn().lock().unwrap();
91 conn.execute(
92 "INSERT INTO db_accounts
93 (id, name, provider, kind, display_name, secret_ref, context,
94 status, created_at, updated_at)
95 VALUES ('acct-broken', 'broken', 'claude', 'oauth', '',
96 '{\"backend\":\"oauth_config_dir\",\"dir\":\"C:\\Users\\a\\claude\"}',
97 '{}', 'unknown', 0, 0)",
98 [],
99 )
100 .unwrap();
101 }
102 drop(store);
103
104 M0019RepairMalformedSecretRef.up(&ctx_for(shared.path())).unwrap();
105
106 let store = Store::open_shared(shared.path()).unwrap();
107 let accounts = store.identity_list(None).unwrap();
108 assert_eq!(accounts.len(), 2, "both the already-good and the repaired row must be visible");
109 assert!(accounts.iter().any(|a| a.id == "acct-broken"));
110 }
111
112 #[test]
113 fn missing_shared_store_is_a_noop() {
114 let missing = std::env::temp_dir().join("agentmux-test-shared-definitely-missing-r7z.db");
115 let _ = std::fs::remove_file(&missing);
116 M0019RepairMalformedSecretRef.up(&ctx_for(&missing)).unwrap();
117 assert!(!missing.exists(), "up() must not create the store");
118 }
119}