feat(worker): build/test the merge preview, not just the post-merge merge

preview_merge could only see file-overlap cleanliness via git merge-tree, never
whether the result compiles -- the two costliest findings of the 2026-08-06 batch
run were both merge-tree-clean but build-broken. When a list has a verify command
configured, a clean preview is now additionally materialized (via a commit-tree +
detached scratch worktree, outside the real repo, always cleaned up) and
built/tested there, without ever touching the real working tree.

preview_merge always attempts a verify run when a command is configured;
preview_merge_set only does when its new runVerify parameter is set (default off),
so a set preview never starts N builds unasked. The post-merge verify gate is
unchanged.
This commit is contained in:
mika kuns
2026-08-10 12:10:17 +02:00
parent 6a2a19cc9e
commit effece3a41
8 changed files with 475 additions and 28 deletions
@@ -61,4 +61,43 @@ public class GitServicePreviewMergeTests : IDisposable
Assert.Equal(headBefore, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim());
Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
}
[Fact]
public async Task PreviewMergeAsync_Clean_ReturnsTreeOidMaterializableIntoDetachedWorktree()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var repo = NewRepo();
var git = new GitService();
var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
File.WriteAllText(Path.Combine(repo.RepoDir, "newfile.txt"), "x\n");
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat");
GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);
var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
Assert.True(preview.Clean);
Assert.False(string.IsNullOrWhiteSpace(preview.TreeOid));
// The tree a clean preview would produce can be wrapped in a commit and checked out into
// a detached, branchless scratch worktree -- the technique the verify-in-preview feature
// relies on -- without ever touching the real working tree.
var parentSha = await git.RevParseAsync(repo.RepoDir, baseBranch, CancellationToken.None);
var commitSha = await git.CommitTreeAsync(repo.RepoDir, preview.TreeOid!, parentSha, "scratch", CancellationToken.None);
var scratchPath = Path.Combine(Path.GetTempPath(), $"scratch_{Guid.NewGuid():N}");
await git.WorktreeAddDetachedAsync(repo.RepoDir, scratchPath, commitSha, CancellationToken.None);
try
{
Assert.True(File.Exists(Path.Combine(scratchPath, "newfile.txt")));
}
finally
{
await git.WorktreeRemoveAsync(repo.RepoDir, scratchPath, force: true, CancellationToken.None);
}
Assert.False(Directory.Exists(scratchPath));
Assert.False(await git.HasChangesAsync(repo.RepoDir, CancellationToken.None));
}
}