feat(worker): add post-merge verification gate for list merges
Per-list optional VerifyCommand (list_config.verify_command) runs via VerifyCommandRunner in the list's working dir right after a successful merge/continue-merge, before the task is allowed to reach Done. A non-zero exit or timeout leaves the merge in place but keeps the task out of Done and reports StatusVerifyFailed with an output excerpt through MergeResultDto/review_task; no command configured behaves exactly as before. Merges against the same repo are now serialized per working dir so a running verify can't be interrupted by a second merge landing mid-build. Adds the field to the List Settings modal (en/de localized) and covers success/failure/timeout in TaskMergeServiceTests + VerifyCommandRunnerTests.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Services;
|
||||
|
||||
public class VerifyCommandRunnerTests
|
||||
{
|
||||
private readonly VerifyCommandRunner _runner = new();
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ExitsZero_ReportsSuccessNotTimedOut()
|
||||
{
|
||||
var result = await _runner.RunAsync(
|
||||
Path.GetTempPath(), "exit 0", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.ExitCode);
|
||||
Assert.False(result.TimedOut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_NonZeroExit_ReportsExitCode()
|
||||
{
|
||||
var result = await _runner.RunAsync(
|
||||
Path.GetTempPath(), "exit 7", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
Assert.Equal(7, result.ExitCode);
|
||||
Assert.False(result.TimedOut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_CapturesStdoutAndStderr()
|
||||
{
|
||||
var result = await _runner.RunAsync(
|
||||
Path.GetTempPath(), "echo hello-out & echo hello-err 1>&2", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
|
||||
Assert.Contains("hello-out", result.Output);
|
||||
Assert.Contains("hello-err", result.Output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_RunsInSpecifiedWorkingDirectory()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"verify_wd_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var result = await _runner.RunAsync(dir, "cd", TimeSpan.FromSeconds(30), CancellationToken.None);
|
||||
Assert.Contains(new DirectoryInfo(dir).Name, result.Output);
|
||||
}
|
||||
finally { Directory.Delete(dir, recursive: true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_ExceedsTimeout_KillsProcessAndReportsTimedOut()
|
||||
{
|
||||
var result = await _runner.RunAsync(
|
||||
Path.GetTempPath(), "ping -n 60 127.0.0.1", TimeSpan.FromMilliseconds(300), CancellationToken.None);
|
||||
|
||||
Assert.True(result.TimedOut);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user