chore(claude-do): merge Unit-Branch Teil 1: Branch-Naming + Basis-Branch pro Liste
ClaudeDo-Task: e202e601-64c0-4ecb-8dc9-2fb0da01a84a
This commit is contained in:
@@ -228,6 +228,23 @@ public class ListSettingsModalViewModelTests
|
||||
Assert.Equal(7, worker.SavedListConfig!.TicketProjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_sends_BaseBranch_explicitly()
|
||||
{
|
||||
var worker = new CountingWorker { Cfg = new ListConfigDto("opus", null, null, BaseBranch: "dev") };
|
||||
var vm = new ListSettingsModalViewModel(worker, NewFactory());
|
||||
|
||||
await vm.LoadAsync("l1", "List One", "/repo", "feat");
|
||||
Assert.Equal("dev", vm.BaseBranch);
|
||||
await vm.SaveCommand.ExecuteAsync(null);
|
||||
Assert.Equal("dev", worker.SavedListConfig!.BaseBranch);
|
||||
|
||||
// "" (not null) so clearing the field really clears the stored branch.
|
||||
vm.BaseBranch = " ";
|
||||
await vm.SaveCommand.ExecuteAsync(null);
|
||||
Assert.Equal("", worker.SavedListConfig!.BaseBranch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_CoreLoadThrows_BlocksSaveAndDoesNotOverwriteConfig()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Worker.Git;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Git;
|
||||
|
||||
public sealed class CompanyRepoTests : IDisposable
|
||||
{
|
||||
private readonly GitRepoFixture _repo = new();
|
||||
public void Dispose() => _repo.Dispose();
|
||||
|
||||
[Theory]
|
||||
[InlineData("https://srv.local/Coll/Proj/_git/Repo", "/_git/", true)]
|
||||
[InlineData("https://mika@srv.local/tfs/Coll/Proj/_git/Mein%20Repo.git", "/_git/", true)]
|
||||
[InlineData("ssh://git@git.kuns.dev:22222/kuns/ClaudeDo.git", "/_git/", false)]
|
||||
[InlineData(null, "/_git/", false)]
|
||||
[InlineData("https://srv.local/Coll/Proj/_git/Repo", "", false)]
|
||||
[InlineData("https://GITHUB.com/acme/repo.git", "github.com/acme", true)]
|
||||
public void Matches(string? url, string pattern, bool expected)
|
||||
=> Assert.Equal(expected, CompanyRepo.Matches(url, pattern));
|
||||
|
||||
[Fact]
|
||||
public async Task IsCompanyAsync_reads_origin()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var git = new GitService();
|
||||
Assert.False(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));
|
||||
|
||||
GitRepoFixture.RunGit(_repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
|
||||
|
||||
Assert.True(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));
|
||||
Assert.Equal("https://srv.local/Coll/Proj/_git/Repo", await git.GetRemoteUrlAsync(_repo.RepoDir));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.External;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Hub;
|
||||
|
||||
/// BaseBranch und PowerUseAllowed sitzen in der SetConfigAsync-kopiert-verbatim-Falle: jeder Writer,
|
||||
/// der sie nicht durchreicht, setzt sie still zurück.
|
||||
public sealed class ListConfigBaseBranchTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
private WorkerHub CreateHub()
|
||||
{
|
||||
var factory = _db.CreateFactory();
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, broadcaster, factory,
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return hub;
|
||||
}
|
||||
|
||||
private async Task<string> SeedListAsync(string? baseBranch, bool powerUse = false)
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
await using var ctx = _db.CreateContext();
|
||||
var repo = new ListRepository(ctx);
|
||||
await repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||
await repo.SetConfigAsync(new ListConfigEntity { ListId = listId, BaseBranch = baseBranch, PowerUseAllowed = powerUse });
|
||||
return listId;
|
||||
}
|
||||
|
||||
private async Task<ListConfigEntity?> ReadConfigAsync(string listId)
|
||||
{
|
||||
await using var ctx = _db.CreateContext();
|
||||
return await new ListRepository(ctx).GetConfigAsync(listId);
|
||||
}
|
||||
|
||||
private ConfigMcpTools CreateTool(ClaudeDoDbContext ctx) =>
|
||||
new(new ListRepository(ctx), new TaskRepository(ctx),
|
||||
new HubBroadcaster(new CapturingHubContext()), _db.CreateFactory());
|
||||
|
||||
[Fact]
|
||||
public async Task set_list_config_preserves_BaseBranch_and_PowerUseAllowed()
|
||||
{
|
||||
var listId = await SeedListAsync("dev", powerUse: true);
|
||||
await using var ctx = _db.CreateContext();
|
||||
|
||||
await CreateTool(ctx).SetListConfig(listId, model: "opus", cancellationToken: CancellationToken.None);
|
||||
|
||||
var cfg = await ReadConfigAsync(listId);
|
||||
Assert.Equal("dev", cfg!.BaseBranch);
|
||||
Assert.True(cfg.PowerUseAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task get_list_config_returns_BaseBranch()
|
||||
{
|
||||
var listId = await SeedListAsync("dev");
|
||||
await using var ctx = _db.CreateContext();
|
||||
|
||||
var result = await CreateTool(ctx).GetListConfig(listId, CancellationToken.None);
|
||||
|
||||
Assert.Equal("dev", result.Config!.BaseBranch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateListConfig_null_keeps_BaseBranch()
|
||||
{
|
||||
var listId = await SeedListAsync("dev");
|
||||
await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null));
|
||||
Assert.Equal("dev", (await ReadConfigAsync(listId))!.BaseBranch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateListConfig_empty_clears_BaseBranch()
|
||||
{
|
||||
var listId = await SeedListAsync("dev");
|
||||
await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null, BaseBranch: ""));
|
||||
Assert.Null((await ReadConfigAsync(listId))!.BaseBranch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateListConfig_sets_BaseBranch_and_GetListConfig_returns_it()
|
||||
{
|
||||
var listId = await SeedListAsync(null);
|
||||
var hub = CreateHub();
|
||||
await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, BaseBranch: "dev"));
|
||||
Assert.Equal("dev", (await hub.GetListConfig(listId))!.BaseBranch);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,18 @@ public sealed class UntrackedWorktreeRecoveryTests : IDisposable
|
||||
Assert.Equal(registered, untracked);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\src\.claudedo-worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
|
||||
[InlineData(@"C:\src\.claudedo-worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
[InlineData(@"C:\Users\x\.claudeDo\worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
|
||||
[InlineData(@"C:\Users\x\.claudeDo\worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
[InlineData(@"C:\src\my-own-worktree", @"C:\Users\x\.claudeDo\worktrees", false)]
|
||||
public void IsTaskWorktreePath(string path, string centralRoot, bool expected)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
Assert.Equal(expected, UntrackedWorktreeRecovery.IsTaskWorktreePath(path, centralRoot));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_NoLists_DoesNotThrow()
|
||||
{
|
||||
@@ -62,6 +74,7 @@ public sealed class UntrackedWorktreeRecoveryTests : IDisposable
|
||||
_db.CreateFactory(),
|
||||
new ClaudeDo.Data.Git.GitService(),
|
||||
new HubBroadcaster(hubContext),
|
||||
new ClaudeDo.Worker.Config.WorkerConfig(),
|
||||
NullLogger<UntrackedWorktreeRecovery>.Instance);
|
||||
|
||||
await sut.StartAsync(CancellationToken.None);
|
||||
|
||||
@@ -214,6 +214,39 @@ public class PlanningAggregatorTests : IDisposable
|
||||
Assert.DoesNotContain(branches, b => b == ok.Value.IntegrationBranch);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, "main")]
|
||||
[InlineData("dev", "dev")]
|
||||
public async Task CleanupIntegrationBranchAsync_ReturnsToBaseBranchOrMain_NotATaskBranch(string? baseBranch, string expected)
|
||||
{
|
||||
var db = NewDb();
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "dev");
|
||||
var (parentId, _, _) = await SeedPlanningWithTwoChildrenAsync(db, repo);
|
||||
|
||||
// Sorts before main and is checked out in a worktree — the old guess picked it and the checkout threw.
|
||||
var taskWt = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
_wtCleanups.Add((repo.RepoDir, taskWt));
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", "chore/1-task", taskWt, repo.BaseCommit);
|
||||
|
||||
if (baseBranch is not null)
|
||||
{
|
||||
using var ctx = db.CreateContext();
|
||||
var listId = (await new TaskRepository(ctx).GetByIdAsync(parentId))!.ListId;
|
||||
await new ListRepository(ctx).SetConfigAsync(new ListConfigEntity { ListId = listId, BaseBranch = baseBranch });
|
||||
}
|
||||
|
||||
var git = new GitService();
|
||||
var svc = new PlanningAggregator(db.CreateFactory(), git, NullLogger<PlanningAggregator>.Instance);
|
||||
var built = Assert.IsType<CombinedDiffResult.Ok>(await svc.BuildIntegrationBranchAsync(parentId, "main", CancellationToken.None));
|
||||
Assert.Equal(built.Value.IntegrationBranch, await git.GetCurrentBranchAsync(repo.RepoDir));
|
||||
|
||||
await svc.CleanupIntegrationBranchAsync(parentId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(expected, await git.GetCurrentBranchAsync(repo.RepoDir));
|
||||
}
|
||||
|
||||
private void SeedWorktreeWithFile(ClaudeDoDbContext ctx, GitRepoFixture repo, string taskId, string filename, string content)
|
||||
{
|
||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
|
||||
@@ -28,6 +28,12 @@ public class CommitMessageBuilderTests
|
||||
Assert.Equal("abc", CommitMessageBuilder.ToSlug("--abc--"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToSlug_transliterates_umlauts()
|
||||
{
|
||||
Assert.Equal("aerger-ueber-groesse-strasse", CommitMessageBuilder.ToSlug("Ärger über Größe Straße"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Title_TruncatedTo60()
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ public class WorktreeManagerTests : IDisposable
|
||||
Assert.NotNull(ctx);
|
||||
Assert.True(Directory.Exists(ctx.WorktreePath));
|
||||
// StartsWith: a folder leaked into the shared %TEMP% root by another test forces the id suffix.
|
||||
Assert.StartsWith($"claudedo/{task.Number}-test-task", ctx.BranchName);
|
||||
Assert.StartsWith($"chore/{task.Number}-test-task", ctx.BranchName);
|
||||
Assert.StartsWith($"{task.Number}-test-task", Path.GetFileName(ctx.WorktreePath));
|
||||
Assert.Equal(repo.BaseCommit, ctx.BaseCommit);
|
||||
|
||||
@@ -80,14 +80,33 @@ public class WorktreeManagerTests : IDisposable
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Fix: Löschen des Tasks!", "7-fix-lschen-des-tasks")]
|
||||
[InlineData("äöü", "7")]
|
||||
[InlineData("a very long title that goes well beyond the forty char cap", "7-a-very-long-title-that-goes-well-beyond")]
|
||||
public void NameFor_NumberPlusCappedTitleSlug(string title, string expected)
|
||||
[InlineData("Fix: Löschen des Tasks!", null, false, "7-fix-loeschen-des-tasks")]
|
||||
[InlineData("äöü", null, false, "7-aeoeue")]
|
||||
[InlineData("a very long title that goes well beyond the forty char cap", null, false, "7-a-very-long-title-that-goes-well-beyond")]
|
||||
[InlineData("test task", 412, false, "412-test-task")]
|
||||
[InlineData("test task", 412, true, "412-test-task")]
|
||||
[InlineData("test task", null, true, "test-task")]
|
||||
[InlineData("!!!", null, true, "7")]
|
||||
public void NameFor_NumberRules(string title, int? ticketId, bool isCompany, string expected)
|
||||
{
|
||||
Assert.Equal(expected, WorktreeManager.NameFor(new TaskEntity { Id = "x", ListId = "y", Title = title, Number = 7, CreatedAt = DateTime.UtcNow }));
|
||||
var task = new TaskEntity { Id = "x", ListId = "y", Title = title, Number = 7, CreatedAt = DateTime.UtcNow };
|
||||
Assert.Equal(expected, WorktreeManager.NameFor(task, ticketId, isCompany));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("feat", "feature")]
|
||||
[InlineData("fix", "fix")]
|
||||
[InlineData("refactor", "refactor")]
|
||||
[InlineData("docs", "docs")]
|
||||
[InlineData("test", "test")]
|
||||
[InlineData("chore", "chore")]
|
||||
[InlineData("ci", "chore")]
|
||||
[InlineData("perf", "chore")]
|
||||
[InlineData(" FEAT ", "feature")]
|
||||
[InlineData("", "chore")]
|
||||
public void BranchTypeFor_maps_commit_types(string commitType, string expected)
|
||||
=> Assert.Equal(expected, WorktreeManager.BranchTypeFor(commitType));
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_BroadcastsCreatingWorktreePhase()
|
||||
{
|
||||
@@ -128,14 +147,14 @@ public class WorktreeManagerTests : IDisposable
|
||||
var (mgr, _) = await CreateManagerAsync(task, list);
|
||||
|
||||
// A leftover from an earlier DB whose task happened to get the same number and title.
|
||||
var foreign = $"claudedo/{WorktreeManager.NameFor(task)}";
|
||||
var foreign = $"chore/{WorktreeManager.NameFor(task, null, false)}";
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", foreign, repo.BaseCommit);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
var suffixed = $"{WorktreeManager.NameFor(task)}-{task.Id.Replace("-", "")[..8]}";
|
||||
Assert.Equal($"claudedo/{suffixed}", ctx.BranchName);
|
||||
var suffixed = $"{WorktreeManager.NameFor(task, null, false)}-{task.Id.Replace("-", "")[..8]}";
|
||||
Assert.Equal($"chore/{suffixed}", ctx.BranchName);
|
||||
Assert.Equal(suffixed, Path.GetFileName(ctx.WorktreePath));
|
||||
Assert.False(string.IsNullOrWhiteSpace(GitRepoFixture.RunGit(repo.RepoDir, "branch", "--list", foreign)));
|
||||
}
|
||||
@@ -157,7 +176,7 @@ public class WorktreeManagerTests : IDisposable
|
||||
|
||||
// Occupy the plain name (-> id-suffixed fallback) and the fallback too, so
|
||||
// `git worktree add -b` hits the "already exists" self-heal path.
|
||||
var branchName = $"claudedo/{WorktreeManager.NameFor(task)}";
|
||||
var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", branchName, repo.BaseCommit);
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", $"{branchName}-{task.Id.Replace("-", "")[..8]}", repo.BaseCommit);
|
||||
|
||||
@@ -311,13 +330,13 @@ public class WorktreeManagerTests : IDisposable
|
||||
await Assert.ThrowsAnyAsync<Exception>(
|
||||
() => mgr.CreateAsync(task, list, CancellationToken.None));
|
||||
|
||||
var branchName = $"claudedo/{WorktreeManager.NameFor(task)}";
|
||||
var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";
|
||||
var branchList = GitRepoFixture.RunGit(repo.RepoDir, "branch", "--list", branchName);
|
||||
Assert.True(string.IsNullOrWhiteSpace(branchList),
|
||||
$"orphaned branch {branchName} should be cleaned up, got: {branchList}");
|
||||
|
||||
var worktreeList = GitRepoFixture.RunGit(repo.RepoDir, "worktree", "list");
|
||||
Assert.DoesNotContain(WorktreeManager.NameFor(task), worktreeList);
|
||||
Assert.DoesNotContain(WorktreeManager.NameFor(task, null, false), worktreeList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -340,6 +359,96 @@ public class WorktreeManagerTests : IDisposable
|
||||
Assert.True(Directory.Exists(ctx.WorktreePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_CompanyRepoWithTicket_UsesTicketIdAndFeaturePrefix()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
|
||||
var (task, list) = MakeEntities(repo.RepoDir);
|
||||
task.CommitType = "feat";
|
||||
task.TicketRef = "bandel:412";
|
||||
var (mgr, _) = await CreateManagerAsync(task, list);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
Assert.StartsWith("feature/412-test-task", ctx.BranchName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_CompanyRepoWithoutTicket_OmitsNumber()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
|
||||
var (task, list) = MakeEntities(repo.RepoDir);
|
||||
var (mgr, _) = await CreateManagerAsync(task, list);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
Assert.StartsWith("chore/test-task", ctx.BranchName);
|
||||
Assert.DoesNotContain($"/{task.Number}-", ctx.BranchName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_ChildWithoutTicket_UsesParentTicket()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
var (child, list) = MakeEntities(repo.RepoDir);
|
||||
var parent = new TaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "parent", CommitType = "feat",
|
||||
TicketRef = "bandel:77", CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
child.ParentTaskId = parent.Id;
|
||||
|
||||
var db = new DbFixture();
|
||||
_dbFixtures.Add(db);
|
||||
using (var seedCtx = db.CreateContext())
|
||||
{
|
||||
await new ListRepository(seedCtx).AddAsync(list);
|
||||
await new TaskRepository(seedCtx).AddAsync(parent);
|
||||
await new TaskRepository(seedCtx).AddAsync(child);
|
||||
}
|
||||
var mgr = new WorktreeManager(new GitService(), db.CreateFactory(),
|
||||
new WorkerConfig { WorktreeRootStrategy = "sibling" }, NullLogger<WorktreeManager>.Instance);
|
||||
|
||||
var ctx = await mgr.CreateAsync(child, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
Assert.StartsWith("chore/77-test-task", ctx.BranchName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_BaseBranchSet_BasesOnItEvenWhenAnotherBranchIsCheckedOut()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "dev");
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "dev.txt"), "dev");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "dev work");
|
||||
var devHead = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "elsewhere", repo.BaseCommit);
|
||||
|
||||
var (task, list) = MakeEntities(repo.RepoDir);
|
||||
var (mgr, db) = await CreateManagerAsync(task, list);
|
||||
using (var ctx0 = db.CreateContext())
|
||||
await new ListRepository(ctx0).SetConfigAsync(new ListConfigEntity { ListId = list.Id, BaseBranch = "dev" });
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
Assert.Equal(devHead, ctx.BaseCommit);
|
||||
}
|
||||
|
||||
private static (TaskEntity task, ListEntity list) MakeEntities(string workingDir)
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
|
||||
@@ -1092,6 +1092,74 @@ public class TaskMergeServiceTests : IDisposable
|
||||
Assert.False(Directory.Exists(wtCtx.WorktreePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTargetsAsync_prefers_list_base_branch()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "dev");
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||
using (var ctx = db.CreateContext())
|
||||
await new ListRepository(ctx).SetConfigAsync(new ListConfigEntity { ListId = list.Id, BaseBranch = "dev" });
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var targets = await svc.GetTargetsAsync(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.NotEqual("dev", await new GitService().GetCurrentBranchAsync(repo.RepoDir));
|
||||
Assert.Equal("dev", targets.DefaultBranch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_EmptyTarget_MergesIntoListBaseBranch()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "dev");
|
||||
var original = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
using (var ctx0 = db.CreateContext())
|
||||
await new ListRepository(ctx0).SetConfigAsync(new ListConfigEntity { ListId = list.Id, BaseBranch = "dev" });
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "new\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var result = await svc.ApproveAndMergeAsync(task.Id, "", CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
Assert.Contains("added.txt", GitRepoFixture.RunGit(repo.RepoDir, "ls-tree", "--name-only", "dev"));
|
||||
Assert.Equal(repo.BaseCommit, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", original).Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeAsync_EmptyTarget_MergesIntoCheckedOutBranch()
|
||||
{
|
||||
// PlanningMergeOrchestrator.DrainAsync passes an empty target straight through — that used
|
||||
// to end in `git checkout ""` -> Blocked.
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var current = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, repo.RepoDir, TaskStatus.WaitingForReview);
|
||||
|
||||
var wtMgr = BuildWorktreeManager(db);
|
||||
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
|
||||
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "new\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var result = await svc.MergeAsync(task.Id, "", false, "msg", CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
|
||||
Assert.Contains("added.txt", GitRepoFixture.RunGit(repo.RepoDir, "ls-tree", "--name-only", current));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApproveAndMergeAsync_Conflict_LeavesTaskWaitingForReview()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user