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