Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion PolyPilot.Tests/ConnectionSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public void DefaultValues_NewFields_AreCorrect()
Assert.Null(settings.ServerPassword);
Assert.False(settings.DirectSharingEnabled);
Assert.Equal(CliSourceMode.BuiltIn, settings.CliSource);
Assert.Null(settings.RepositoryStorageRoot);
}

[Fact]
Expand All @@ -154,7 +155,8 @@ public void Save_Load_RoundTrip_WithNewFields()
Port = 4321,
ServerPassword = "mypass",
DirectSharingEnabled = true,
CliSource = CliSourceMode.System
CliSource = CliSourceMode.System,
RepositoryStorageRoot = "D:\\DevDrive\\PolyPilot"
};

var json = JsonSerializer.Serialize(original);
Expand All @@ -164,6 +166,7 @@ public void Save_Load_RoundTrip_WithNewFields()
Assert.Equal("mypass", loaded!.ServerPassword);
Assert.True(loaded.DirectSharingEnabled);
Assert.Equal(CliSourceMode.System, loaded.CliSource);
Assert.Equal("D:\\DevDrive\\PolyPilot", loaded.RepositoryStorageRoot);
}

[Fact]
Expand All @@ -178,6 +181,19 @@ public void BackwardCompatibility_OldJsonWithoutNewFields()
Assert.Null(loaded.ServerPassword);
Assert.False(loaded.DirectSharingEnabled);
Assert.Equal(CliSourceMode.BuiltIn, loaded.CliSource);
Assert.Null(loaded.RepositoryStorageRoot);
}

[Fact]
public void NormalizeRepositoryStorageRoot_Whitespace_ReturnsNull()
{
Assert.Null(ConnectionSettings.NormalizeRepositoryStorageRoot(" "));
}

[Fact]
public void NormalizeRepositoryStorageRoot_TrimmedPath_ReturnsTrimmed()
{
Assert.Equal("C:\\Dev", ConnectionSettings.NormalizeRepositoryStorageRoot(" C:\\Dev "));
}

[Fact]
Expand Down
130 changes: 130 additions & 0 deletions PolyPilot.Tests/RepoManagerStorageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.Reflection;
using System.Text.Json;
using PolyPilot.Models;
using PolyPilot.Services;

namespace PolyPilot.Tests;

public class RepoManagerStorageTests
{
private static RepoManager CreateRepoManagerWithState(RepositoryState state)
{
var rm = new RepoManager();
var stateField = typeof(RepoManager).GetField("_state", BindingFlags.NonPublic | BindingFlags.Instance)!;
var loadedField = typeof(RepoManager).GetField("_loaded", BindingFlags.NonPublic | BindingFlags.Instance)!;
stateField.SetValue(rm, state);
loadedField.SetValue(rm, true);
return rm;
}

private static void InvokeBackfillWorktreeClonePaths(RepoManager rm, RepositoryInfo repo)
{
var method = typeof(RepoManager).GetMethod("BackfillWorktreeClonePaths", BindingFlags.NonPublic | BindingFlags.Instance)!;
method.Invoke(rm, new object[] { repo });
}

[Fact]
public void BackfillWorktreeClonePaths_FillsMissingPaths()
{
var repo = new RepositoryInfo { Id = "owner-repo", BareClonePath = "/old/repos/owner-repo.git" };
var state = new RepositoryState
{
Repositories = new List<RepositoryInfo> { repo },
Worktrees = new List<WorktreeInfo>
{
new() { Id = "wt1", RepoId = "owner-repo", Path = "/old/worktrees/owner-repo-a1b2c3d4" },
new() { Id = "wt2", RepoId = "other-repo", Path = "/old/worktrees/other-repo-e5f6g7h8" }
}
};
var rm = CreateRepoManagerWithState(state);

InvokeBackfillWorktreeClonePaths(rm, repo);

Assert.Equal("/old/repos/owner-repo.git", state.Worktrees[0].BareClonePath);
Assert.Null(state.Worktrees[1].BareClonePath);
}

[Fact]
public void BackfillWorktreeClonePaths_DoesNotOverwriteExistingPath()
{
var repo = new RepositoryInfo { Id = "owner-repo", BareClonePath = "/new/repos/owner-repo.git" };
var state = new RepositoryState
{
Repositories = new List<RepositoryInfo> { repo },
Worktrees = new List<WorktreeInfo>
{
new()
{
Id = "wt1",
RepoId = "owner-repo",
Path = "/old/worktrees/owner-repo-a1b2c3d4",
BareClonePath = "/old/repos/owner-repo.git"
}
}
};
var rm = CreateRepoManagerWithState(state);

InvokeBackfillWorktreeClonePaths(rm, repo);

Assert.Equal("/old/repos/owner-repo.git", state.Worktrees[0].BareClonePath);
}

[Fact]
public void RepositoryState_WorktreeBareClonePath_RoundTripsJson()
{
var original = new RepositoryState
{
Repositories = new List<RepositoryInfo>
{
new() { Id = "owner-repo", BareClonePath = "/repos/owner-repo.git", Url = "https://github.com/owner/repo" }
},
Worktrees = new List<WorktreeInfo>
{
new() { Id = "wt1", RepoId = "owner-repo", Path = "/worktrees/owner-repo-1234", BareClonePath = "/repos/owner-repo.git" }
}
};

var json = JsonSerializer.Serialize(original);
var roundTrip = JsonSerializer.Deserialize<RepositoryState>(json);

Assert.NotNull(roundTrip);
Assert.Single(roundTrip!.Worktrees);
Assert.Equal("/repos/owner-repo.git", roundTrip.Worktrees[0].BareClonePath);
}

[Fact]
public async Task RecloneAllRepositoriesToCurrentRootAsync_NoRepos_Completes()
{
var rm = CreateRepoManagerWithState(new RepositoryState());
await rm.RecloneAllRepositoriesToCurrentRootAsync();
Assert.Empty(rm.Repositories);
}

[Fact]
public async Task RecloneAllRepositoriesToCurrentRootAsync_NullBareClonePath_DoesNotThrow()
{
// Repos with null/empty BareClonePath should not crash EnsureRepoCloneInCurrentRootAsync.
// They'll attempt a fresh clone (which will fail without network), but the error is caught
// and remaining repos continue processing.
var repoWithNullPath = new RepositoryInfo { Id = "owner-repo", BareClonePath = null!, Url = "https://github.com/owner/repo" };
var state = new RepositoryState { Repositories = new List<RepositoryInfo> { repoWithNullPath } };
var rm = CreateRepoManagerWithState(state);
var progressMessages = new List<string>();

// Should not throw ArgumentNullException even with null BareClonePath
await rm.RecloneAllRepositoriesToCurrentRootAsync(msg => progressMessages.Add(msg));

// A warning progress message is emitted when the clone fails (network unavailable in tests)
Assert.Contains(progressMessages, m => m.StartsWith("⚠") || m.Contains("owner-repo") || m.Contains("[1/1]"));
}

[Fact]
public void PathsEqual_IgnoresTrailingSeparator()
{
var method = typeof(RepoManager).GetMethod("PathsEqual", BindingFlags.NonPublic | BindingFlags.Static)!;
var root = Path.Combine(Path.GetTempPath(), "polypilot-repo-root");
var withTrailing = root + Path.DirectorySeparatorChar;
var equal = (bool)method.Invoke(null, new object[] { withTrailing, root })!;
Assert.True(equal);
}
}
Loading