@@ -497,6 +497,118 @@ private static void ForceDeleteDirectory(string path)
497497 Directory . Delete ( path , true ) ;
498498 }
499499
500+ [ Fact ]
501+ public async Task AddRepositoryFromLocal_LocalRepoId_HasExpectedFormat ( )
502+ {
503+ // The local repo ID should follow the pattern "{baseId}-local-{pathHash}"
504+ // where pathHash is a hex-encoded hash of the normalized path.
505+ var tempDir = Path . Combine ( Path . GetTempPath ( ) , $ "local-id-format-test-{ Guid . NewGuid ( ) : N} ") ;
506+ var testBaseDir = Path . Combine ( Path . GetTempPath ( ) , $ "rmtest-{ Guid . NewGuid ( ) : N} ") ;
507+ Directory . CreateDirectory ( tempDir ) ;
508+ Directory . CreateDirectory ( testBaseDir ) ;
509+ try
510+ {
511+ var remoteUrl = "https://github.com/test-owner/id-format-test.git" ;
512+
513+ await RunProcess ( "git" , "init" , tempDir ) ;
514+ await RunProcess ( "git" , "-C" , tempDir , "config" , "user.email" , "test@test.com" ) ;
515+ await RunProcess ( "git" , "-C" , tempDir , "config" , "user.name" , "Test" ) ;
516+ await RunProcess ( "git" , "-C" , tempDir , "commit" , "--allow-empty" , "-m" , "init" ) ;
517+ await RunProcess ( "git" , "-C" , tempDir , "remote" , "add" , "origin" , remoteUrl ) ;
518+
519+ var rm = new RepoManager ( ) ;
520+ RepoManager . SetBaseDirForTesting ( testBaseDir ) ;
521+ try
522+ {
523+ // Pre-create a URL-based repo so the local one gets a distinct ID
524+ var urlId = RepoManager . RepoIdFromUrl ( remoteUrl ) ;
525+ var barePath = Path . Combine ( testBaseDir , "repos" , $ "{ urlId } .git") ;
526+ Directory . CreateDirectory ( barePath ) ;
527+ var state = new RepositoryState ( ) ;
528+ state . Repositories . Add ( new RepositoryInfo
529+ {
530+ Id = urlId , Name = "id-format-test" ,
531+ Url = remoteUrl , BareClonePath = barePath , AddedAt = DateTime . UtcNow
532+ } ) ;
533+ File . WriteAllText ( Path . Combine ( testBaseDir , "repos.json" ) ,
534+ System . Text . Json . JsonSerializer . Serialize ( state ) ) ;
535+ rm . Load ( ) ;
536+
537+ var localRepo = await rm . AddRepositoryFromLocalAsync ( tempDir ) ;
538+
539+ // ID should match pattern: baseId-local-HEXHASH
540+ Assert . Matches ( @"^test-owner-id-format-test-local-[0-9a-f]{8}$" , localRepo . Id ) ;
541+ }
542+ finally { RepoManager . SetBaseDirForTesting ( TestSetup . TestBaseDir ) ; }
543+ }
544+ finally
545+ {
546+ ForceDeleteDirectory ( tempDir ) ;
547+ ForceDeleteDirectory ( testBaseDir ) ;
548+ }
549+ }
550+
551+ [ Fact ]
552+ public void EnsureRepoClone_SkipsCloneForNonBareRepo_WithGitDirectory ( )
553+ {
554+ // EnsureRepoCloneInCurrentRootAsync should detect a .git directory
555+ // and skip clone management for repos added via "Existing Folder".
556+ // This is a structural test that verifies the guard exists.
557+ var sourceFile = File . ReadAllText ( Path . Combine ( GetRepoRoot ( ) , "PolyPilot" , "Services" , "RepoManager.cs" ) ) ;
558+ var methodBody = ExtractMethodBody ( sourceFile , "EnsureRepoCloneInCurrentRootAsync" ) ;
559+
560+ // Must check for both .git directory and .git file (worktree checkout)
561+ Assert . Contains ( "Directory.Exists(Path.Combine(repo.BareClonePath, \" .git\" ))" , methodBody ) ;
562+ Assert . Contains ( "File.Exists(Path.Combine(repo.BareClonePath, \" .git\" ))" , methodBody ) ;
563+ }
564+
565+ [ Fact ]
566+ public async Task AddRepositoryFromLocal_ValidationErrors_ThrowDescriptiveExceptions ( )
567+ {
568+ var nonExistent = Path . Combine ( Path . GetTempPath ( ) , $ "does-not-exist-{ Guid . NewGuid ( ) : N} ") ;
569+ var notGit = Path . Combine ( Path . GetTempPath ( ) , $ "not-git-{ Guid . NewGuid ( ) : N} ") ;
570+ var noOrigin = Path . Combine ( Path . GetTempPath ( ) , $ "no-origin-{ Guid . NewGuid ( ) : N} ") ;
571+ var testBaseDir = Path . Combine ( Path . GetTempPath ( ) , $ "rmtest-{ Guid . NewGuid ( ) : N} ") ;
572+ Directory . CreateDirectory ( notGit ) ;
573+ Directory . CreateDirectory ( noOrigin ) ;
574+ Directory . CreateDirectory ( testBaseDir ) ;
575+ try
576+ {
577+ // Initialize noOrigin as git repo but without origin remote
578+ await RunProcess ( "git" , "init" , noOrigin ) ;
579+ await RunProcess ( "git" , "-C" , noOrigin , "config" , "user.email" , "test@test.com" ) ;
580+ await RunProcess ( "git" , "-C" , noOrigin , "config" , "user.name" , "Test" ) ;
581+ await RunProcess ( "git" , "-C" , noOrigin , "commit" , "--allow-empty" , "-m" , "init" ) ;
582+
583+ var rm = new RepoManager ( ) ;
584+ RepoManager . SetBaseDirForTesting ( testBaseDir ) ;
585+ try
586+ {
587+ // Non-existent folder
588+ var ex1 = await Assert . ThrowsAsync < InvalidOperationException > (
589+ ( ) => rm . AddRepositoryFromLocalAsync ( nonExistent ) ) ;
590+ Assert . Contains ( "not found" , ex1 . Message , StringComparison . OrdinalIgnoreCase ) ;
591+
592+ // Folder that isn't a git repo
593+ var ex2 = await Assert . ThrowsAsync < InvalidOperationException > (
594+ ( ) => rm . AddRepositoryFromLocalAsync ( notGit ) ) ;
595+ Assert . Contains ( "not a git repository" , ex2 . Message , StringComparison . OrdinalIgnoreCase ) ;
596+
597+ // Git repo without origin remote
598+ var ex3 = await Assert . ThrowsAsync < InvalidOperationException > (
599+ ( ) => rm . AddRepositoryFromLocalAsync ( noOrigin ) ) ;
600+ Assert . Contains ( "origin" , ex3 . Message , StringComparison . OrdinalIgnoreCase ) ;
601+ }
602+ finally { RepoManager . SetBaseDirForTesting ( TestSetup . TestBaseDir ) ; }
603+ }
604+ finally
605+ {
606+ ForceDeleteDirectory ( notGit ) ;
607+ ForceDeleteDirectory ( noOrigin ) ;
608+ ForceDeleteDirectory ( testBaseDir ) ;
609+ }
610+ }
611+
500612 private static string GetRepoRoot ( )
501613 {
502614 var dir = new DirectoryInfo ( AppContext . BaseDirectory ) ;
0 commit comments