@@ -491,45 +491,132 @@ fn apply_top_level_db(cli: &mut Cli) {
491491 }
492492}
493493
494+ /// Outcome of resolving the default database path when no `--db`/`$MIMIR_DB_PATH`
495+ /// was given: the chosen path plus any *other* existing candidate databases that
496+ /// were passed over. When `other_candidates` is non-empty the caller should warn
497+ /// so an ambiguous multi-DB state is visible rather than silent (#421).
498+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
499+ struct DbResolution {
500+ chosen : String ,
501+ other_candidates : Vec < String > ,
502+ }
503+
504+ /// Pure, testable core of default DB-path resolution (#421).
505+ ///
506+ /// Given the home directory and an existence check, decides which database the
507+ /// server should open when the user did not pass `--db` or set `$MIMIR_DB_PATH`.
508+ ///
509+ /// Precedence (first existing wins):
510+ /// 1. `~/.mimir/data/perseus-vault.db` (canonical, current name)
511+ /// 2. `~/.mimir/data/mneme.db` (pre-rename)
512+ /// 3. `~/.mimir/data/mimir.db` (pre-rename)
513+ /// 4. `~/mimir.db` (legacy single-user install location)
514+ /// If none exist, fall back to creating (1), the canonical path.
515+ ///
516+ /// Crucially `~/mimir.db` is chosen *before* falling through to create a fresh
517+ /// canonical DB, so an existing single-user install is picked up instead of
518+ /// silently starting empty. `other_candidates` reports every *other* database
519+ /// that also exists so the caller can warn about the ambiguity.
520+ fn resolve_default_db ( home : & str , exists : & dyn Fn ( & str ) -> bool ) -> DbResolution {
521+ let dir = format ! ( "{}/.mimir/data" , home) ;
522+ let vault_path = format ! ( "{}/perseus-vault.db" , dir) ;
523+ let mneme_path = format ! ( "{}/mneme.db" , dir) ;
524+ let mimir_path = format ! ( "{}/mimir.db" , dir) ;
525+ let home_legacy_path = format ! ( "{}/mimir.db" , home) ;
526+
527+ // Ordered candidate list; the first that exists is chosen.
528+ let candidates = [
529+ vault_path. clone ( ) ,
530+ mneme_path,
531+ mimir_path,
532+ home_legacy_path,
533+ ] ;
534+
535+ let existing: Vec < String > = candidates
536+ . iter ( )
537+ . filter ( |p| exists ( p) )
538+ . cloned ( )
539+ . collect ( ) ;
540+
541+ // Chosen: first existing candidate in precedence order, else the canonical
542+ // path (which will be created fresh).
543+ let chosen = existing. first ( ) . cloned ( ) . unwrap_or ( vault_path) ;
544+ let other_candidates = existing
545+ . into_iter ( )
546+ . filter ( |p| * p != chosen)
547+ . collect ( ) ;
548+
549+ DbResolution {
550+ chosen,
551+ other_candidates,
552+ }
553+ }
554+
494555/// Resolve the default database path.
495556///
496557/// Perseus Vault rename: fresh installs default to `perseus-vault.db`. If a
497- /// pre-rename `mneme.db` or `mimir.db` already exists at the same directory
498- /// (and no `perseus-vault.db` does), we keep using it so upgraders don't
499- /// silently start over with an empty database — same fallback shape as the
500- /// legacy `~/mimir.db` -> `~/.mimir/data/`
501- /// move handled by `check_legacy_db` below.
558+ /// pre-rename `mneme.db`/`mimir.db`, or a legacy single-user `~/mimir.db`,
559+ /// already exists we keep using it so upgraders don't silently start over with
560+ /// an empty database (#421).
561+ ///
562+ /// This is intentionally side-effect free apart from creating the data dir: it
563+ /// is used both as clap's `default_value_t` (evaluated eagerly, even when the
564+ /// user passes `--db`) and in equality comparisons by `apply_top_level_db`, so
565+ /// it must NOT print warnings. The multi-candidate split-brain warning is
566+ /// emitted separately by `check_legacy_db`, which runs only at real startup and
567+ /// only when the default path was actually used.
502568fn default_db_path ( ) -> String {
503- std:: env:: var ( "MIMIR_DB_PATH" ) . unwrap_or_else ( |_| {
504- let home = std:: env:: var ( "HOME" )
505- . or_else ( |_| std:: env:: var ( "USERPROFILE" ) )
506- . unwrap_or_else ( |_| {
507- eprintln ! ( "perseus-vault: could not determine home directory. Set MIMIR_DB_PATH or HOME/USERPROFILE." ) ;
508- std:: process:: exit ( 1 ) ;
509- } ) ;
510- let dir = format ! ( "{}/.mimir/data" , home) ;
511- let _ = std:: fs:: create_dir_all ( & dir) ;
512- let vault_path = format ! ( "{}/perseus-vault.db" , dir) ;
513- let mneme_path = format ! ( "{}/mneme.db" , dir) ;
514- let mimir_path = format ! ( "{}/mimir.db" , dir) ;
515- if std:: path:: Path :: new ( & vault_path) . exists ( ) {
516- vault_path
517- } else if std:: path:: Path :: new ( & mneme_path) . exists ( ) {
518- mneme_path
519- } else if std:: path:: Path :: new ( & mimir_path) . exists ( ) {
520- mimir_path
521- } else {
522- vault_path
523- }
524- } )
569+ if let Ok ( explicit) = std:: env:: var ( "MIMIR_DB_PATH" ) {
570+ return explicit;
571+ }
572+ let home = std:: env:: var ( "HOME" )
573+ . or_else ( |_| std:: env:: var ( "USERPROFILE" ) )
574+ . unwrap_or_else ( |_| {
575+ eprintln ! ( "perseus-vault: could not determine home directory. Set MIMIR_DB_PATH or HOME/USERPROFILE." ) ;
576+ std:: process:: exit ( 1 ) ;
577+ } ) ;
578+ let dir = format ! ( "{}/.mimir/data" , home) ;
579+ let _ = std:: fs:: create_dir_all ( & dir) ;
580+
581+ resolve_default_db ( & home, & |p| std:: path:: Path :: new ( p) . exists ( ) ) . chosen
525582}
526583
527584/// Check for a legacy database at ~/mimir.db and warn if the default path
528585/// would create a new empty database instead.
586+ ///
587+ /// Also emits the #421 split-brain warning: when the path being used is the
588+ /// resolved *default* (no `--db`, no `$MIMIR_DB_PATH`) and more than one
589+ /// candidate database exists on disk, name the chosen one and the others so the
590+ /// ambiguity is visible rather than silent. When `--db`/`$MIMIR_DB_PATH` is set
591+ /// explicitly, behavior is unchanged and no warning fires.
529592fn check_legacy_db ( db_path : & str ) {
530593 let home = std:: env:: var ( "HOME" )
531594 . or_else ( |_| std:: env:: var ( "USERPROFILE" ) )
532595 . unwrap_or_else ( |_| "/root" . to_string ( ) ) ;
596+
597+ // Was this path chosen implicitly (the resolved default) rather than via
598+ // --db / $MIMIR_DB_PATH? Only then do we own the resolution and warn.
599+ let env_set = std:: env:: var ( "MIMIR_DB_PATH" ) . is_ok ( ) ;
600+ let is_default = !env_set && db_path == default_db_path ( ) ;
601+
602+ // #421: multiple candidate DBs but the user didn't pick one — surface the
603+ // split-brain instead of silently reading/creating one of them.
604+ if is_default {
605+ let resolution = resolve_default_db ( & home, & |p| std:: path:: Path :: new ( p) . exists ( ) ) ;
606+ if !resolution. other_candidates . is_empty ( ) {
607+ eprintln ! (
608+ "perseus-vault: ⚠ multiple candidate databases found; using {}" ,
609+ resolution. chosen
610+ ) ;
611+ for other in & resolution. other_candidates {
612+ eprintln ! ( "perseus-vault: also present (ignored): {}" , other) ;
613+ }
614+ eprintln ! (
615+ "perseus-vault: pass --db <path> or set MIMIR_DB_PATH to choose explicitly and silence this warning."
616+ ) ;
617+ }
618+ }
619+
533620 let legacy = std:: path:: PathBuf :: from ( format ! ( "{}/mimir.db" , home) ) ;
534621 let target = std:: path:: PathBuf :: from ( db_path) ;
535622 if legacy. exists ( ) && !target. exists ( ) {
@@ -1816,6 +1903,82 @@ mod tests {
18161903 assert ! ( cli. command. is_none( ) ) ;
18171904 }
18181905
1906+ // ---- #421: default DB-path resolution (split-brain) ----
1907+
1908+ /// Helper: existence checker over a fixed set of present paths.
1909+ fn present ( set : & [ String ] ) -> impl Fn ( & str ) -> bool + ' _ {
1910+ move |p : & str | set. iter ( ) . any ( |e| e == p)
1911+ }
1912+
1913+ #[ test]
1914+ fn resolve_default_db_picks_home_legacy_over_creating_fresh ( ) {
1915+ // #421 core: only ~/mimir.db exists. It must be selected instead of
1916+ // creating a fresh ~/.mimir/data/perseus-vault.db (the silent
1917+ // split-brain the issue reports).
1918+ let home = "/home/tester" ;
1919+ let home_legacy = format ! ( "{}/mimir.db" , home) ;
1920+ let existing = vec ! [ home_legacy. clone( ) ] ;
1921+ let r = resolve_default_db ( home, & present ( & existing) ) ;
1922+ assert_eq ! ( r. chosen, home_legacy, "should adopt existing ~/mimir.db" ) ;
1923+ assert ! ( r. other_candidates. is_empty( ) ) ;
1924+ }
1925+
1926+ #[ test]
1927+ fn resolve_default_db_prefers_canonical_when_present ( ) {
1928+ // Canonical perseus-vault.db wins over legacy names in precedence order.
1929+ let home = "/home/tester" ;
1930+ let vault = format ! ( "{}/.mimir/data/perseus-vault.db" , home) ;
1931+ let home_legacy = format ! ( "{}/mimir.db" , home) ;
1932+ let existing = vec ! [ vault. clone( ) , home_legacy. clone( ) ] ;
1933+ let r = resolve_default_db ( home, & present ( & existing) ) ;
1934+ assert_eq ! ( r. chosen, vault) ;
1935+ assert_eq ! ( r. other_candidates, vec![ home_legacy] ) ;
1936+ }
1937+
1938+ #[ test]
1939+ fn resolve_default_db_falls_back_to_canonical_when_none_exist ( ) {
1940+ // Fresh install: nothing exists -> create canonical path, no warning.
1941+ let home = "/home/tester" ;
1942+ let vault = format ! ( "{}/.mimir/data/perseus-vault.db" , home) ;
1943+ let r = resolve_default_db ( home, & present ( & [ ] ) ) ;
1944+ assert_eq ! ( r. chosen, vault) ;
1945+ assert ! ( r. other_candidates. is_empty( ) ) ;
1946+ }
1947+
1948+ #[ test]
1949+ fn resolve_default_db_reports_multiple_candidates ( ) {
1950+ // Multiple candidate DBs -> chosen is highest-precedence, others named
1951+ // so the caller can warn about the ambiguity.
1952+ let home = "/home/tester" ;
1953+ let mneme = format ! ( "{}/.mimir/data/mneme.db" , home) ;
1954+ let mimir = format ! ( "{}/.mimir/data/mimir.db" , home) ;
1955+ let home_legacy = format ! ( "{}/mimir.db" , home) ;
1956+ let existing = vec ! [ mneme. clone( ) , mimir. clone( ) , home_legacy. clone( ) ] ;
1957+ let r = resolve_default_db ( home, & present ( & existing) ) ;
1958+ // perseus-vault.db absent -> mneme.db is highest precedence.
1959+ assert_eq ! ( r. chosen, mneme) ;
1960+ assert_eq ! ( r. other_candidates, vec![ mimir, home_legacy] ) ;
1961+ }
1962+
1963+ #[ test]
1964+ fn resolve_default_db_precedence_order_is_stable ( ) {
1965+ // The full documented order: vault > mneme > mimir(dir) > ~/mimir.db.
1966+ let home = "/home/tester" ;
1967+ let vault = format ! ( "{}/.mimir/data/perseus-vault.db" , home) ;
1968+ let mneme = format ! ( "{}/.mimir/data/mneme.db" , home) ;
1969+ let mimir = format ! ( "{}/.mimir/data/mimir.db" , home) ;
1970+ let home_legacy = format ! ( "{}/mimir.db" , home) ;
1971+ let all = vec ! [
1972+ vault. clone( ) ,
1973+ mneme. clone( ) ,
1974+ mimir. clone( ) ,
1975+ home_legacy. clone( ) ,
1976+ ] ;
1977+ let r = resolve_default_db ( home, & present ( & all) ) ;
1978+ assert_eq ! ( r. chosen, vault) ;
1979+ assert_eq ! ( r. other_candidates, vec![ mneme, mimir, home_legacy] ) ;
1980+ }
1981+
18191982 #[ test]
18201983 fn parses_top_level_db_without_subcommand ( ) {
18211984 // Regression: the documented MCP host config is `mimir --db <path>`
0 commit comments