feat(worker): company unit roots start from a fetched origin base, local base fast-forwarded best-effort
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using ClaudeDo.Data.Git;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Git;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.AzureDevOps;
|
||||
|
||||
/// <summary>Spec 2026-10-01 "Basis aktualisieren": a company unit root starts from a fetched origin/<basis>. Real git, local bare repo as origin.</summary>
|
||||
public sealed class CompanyFetchBaseTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
private readonly GitRepoFixture _repo = new();
|
||||
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), $"cd_fetch_{Guid.NewGuid():N}");
|
||||
private readonly string _bare;
|
||||
private readonly string _listName = $"fb{Guid.NewGuid():N}"[..12];
|
||||
private readonly WorkerConfig _cfg;
|
||||
|
||||
public CompanyFetchBaseTests()
|
||||
{
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
GitRepoFixture.RunGit(_repo.RepoDir, "branch", "-m", "main");
|
||||
_bare = Path.Combine(_tempDir, "cdbare_origin.git");
|
||||
GitRepoFixture.RunGit(_tempDir, "clone", "--bare", "-q", _repo.RepoDir, _bare);
|
||||
GitRepoFixture.RunGit(_repo.RepoDir, "remote", "add", "origin", _bare);
|
||||
_cfg = new WorkerConfig { SandboxRoot = _tempDir, LogRoot = _tempDir, WorktreeRootStrategy = "sibling", CompanyRemotePattern = "cdbare_" };
|
||||
|
||||
using var ctx = _db.CreateContext();
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = _listName, WorkingDir = _repo.RepoDir, CreatedAt = DateTime.UtcNow });
|
||||
ctx.ListConfigs.Add(new ListConfigEntity { ListId = "l1", BaseBranch = "main" });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = "t1", ListId = "l1", Title = "Root", Number = 1, CommitType = "feat",
|
||||
Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(Path.Combine(WorktreeRootResolver.ResolveSiblingRoot(_repo.RepoDir), ".claudedo-worktrees", _listName), true); } catch { }
|
||||
_db.Dispose();
|
||||
_repo.Dispose();
|
||||
try { GitRepoFixture.ForceDeleteDirectory(_tempDir); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>Pushes one commit to the bare origin from a scratch clone; returns its sha.</summary>
|
||||
internal static string PushCommitToOrigin(string bare, string tempDir)
|
||||
{
|
||||
var clone = Path.Combine(tempDir, $"clone_{Guid.NewGuid():N}");
|
||||
GitRepoFixture.RunGit(tempDir, "clone", "-q", bare, clone);
|
||||
GitRepoFixture.RunGit(clone, "config", "user.name", "test");
|
||||
GitRepoFixture.RunGit(clone, "config", "user.email", "test@example.com");
|
||||
File.WriteAllText(Path.Combine(clone, "upstream.txt"), "from colleague");
|
||||
GitRepoFixture.RunGit(clone, "add", "-A");
|
||||
GitRepoFixture.RunGit(clone, "commit", "-q", "-m", "upstream work");
|
||||
GitRepoFixture.RunGit(clone, "push", "-q", "origin", "HEAD:main");
|
||||
return GitRepoFixture.RunGit(clone, "rev-parse", "HEAD").Trim();
|
||||
}
|
||||
|
||||
private Task<WorktreeContext> CreateRoot()
|
||||
{
|
||||
using var ctx = _db.CreateContext();
|
||||
var task = ctx.Tasks.Single(t => t.Id == "t1");
|
||||
var list = ctx.Lists.Single();
|
||||
return new WorktreeManager(new GitService(), _db.CreateFactory(), _cfg, NullLogger<WorktreeManager>.Instance)
|
||||
.CreateAsync(task, list, default);
|
||||
}
|
||||
|
||||
private string Head(string rev = "HEAD") => GitRepoFixture.RunGit(_repo.RepoDir, "rev-parse", rev).Trim();
|
||||
|
||||
[Fact]
|
||||
public async Task OriginAhead_RootStartsFromOrigin_AndCheckedOutCleanBaseIsFastForwarded()
|
||||
{
|
||||
var upstream = PushCommitToOrigin(_bare, _tempDir);
|
||||
|
||||
var wt = await CreateRoot();
|
||||
|
||||
Assert.Equal(upstream, wt.BaseCommit);
|
||||
Assert.Equal(upstream, Head());
|
||||
Assert.True(File.Exists(Path.Combine(wt.WorktreePath, "upstream.txt")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DirtyCheckout_StaysUntouched_ButRootStillStartsFromOrigin()
|
||||
{
|
||||
var before = Head();
|
||||
var upstream = PushCommitToOrigin(_bare, _tempDir);
|
||||
var readme = Path.Combine(_repo.RepoDir, "README.md");
|
||||
File.WriteAllText(readme, "local WIP");
|
||||
|
||||
var wt = await CreateRoot();
|
||||
|
||||
Assert.Equal(upstream, wt.BaseCommit);
|
||||
Assert.Equal(before, Head());
|
||||
Assert.Equal("local WIP", File.ReadAllText(readme));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BaseNotCheckedOut_LocalBranchIsFastForwarded_CheckoutUnchanged()
|
||||
{
|
||||
GitRepoFixture.RunGit(_repo.RepoDir, "checkout", "-q", "-b", "other");
|
||||
var upstream = PushCommitToOrigin(_bare, _tempDir);
|
||||
|
||||
var wt = await CreateRoot();
|
||||
|
||||
Assert.Equal(upstream, wt.BaseCommit);
|
||||
Assert.Equal(upstream, Head("main"));
|
||||
Assert.Equal("other", GitRepoFixture.RunGit(_repo.RepoDir, "branch", "--show-current").Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchFailure_DoesNotFailTheTask_RootStartsFromLocalBase()
|
||||
{
|
||||
GitRepoFixture.RunGit(_repo.RepoDir, "remote", "set-url", "origin", Path.Combine(_tempDir, "cdbare_missing.git"));
|
||||
var local = Head("main");
|
||||
|
||||
var wt = await CreateRoot();
|
||||
|
||||
Assert.Equal(local, wt.BaseCommit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrivateRepo_IsNeverFetched()
|
||||
{
|
||||
_cfg.CompanyRemotePattern = "/_git/";
|
||||
var local = Head("main");
|
||||
PushCommitToOrigin(_bare, _tempDir);
|
||||
|
||||
var wt = await CreateRoot();
|
||||
|
||||
Assert.Equal(local, wt.BaseCommit);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ public sealed class GitRepoFixture : IDisposable
|
||||
RunGit(dir, "commit", "-m", "chore: seed");
|
||||
}
|
||||
|
||||
private static void ForceDeleteDirectory(string path)
|
||||
internal static void ForceDeleteDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) return;
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
|
||||
|
||||
Reference in New Issue
Block a user