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; /// Spec 2026-10-01 "Basis aktualisieren": a company unit root starts from a fetched origin/<basis>. Real git, local bare repo as origin. 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 { } } /// Pushes one commit to the bare origin from a scratch clone; returns its sha. 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 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.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); } }