Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs
Mika Kuns 01235d986f feat(worker,data): add git worktree support and conventional commits
GitService (in ClaudeDo.Data so the UI can reuse it) wraps the git CLI:
IsGitRepo, RevParseHead, WorktreeAdd/Remove, HasChanges, AddAll, Commit
(multi-line via -F -), DiffStat, BranchDelete, MergeFfOnly. Throws with
stderr on failure.

WorktreeManager owns the per-task lifecycle: validate working_dir is a
git repo (throws if not, no DB row written), create the worktree at
<repo>/../.claudedo-worktrees/<slug>/<id>/ (or central root per config),
insert the worktrees row. CommitIfChangedAsync skips when there are no
changes, otherwise commits and updates head_commit + diff_stat.

CommitMessageBuilder produces "{type}({list-slug}): {title<=60}" with a
blank-line-separated description (truncated to 400) and a permanent
"ClaudeDo-Task: <id>" trailer. Slug normalises whitespace + strips
non-alphanumerics. Newlines hard-coded to \n so git on Windows doesn't
choke on \r\n.

TaskRunner branches on list.WorkingDir: worktree path runs Claude in the
worktree, commits on success, broadcasts WorktreeUpdated; failure leaves
the worktree row active for inspection. Sandbox path unchanged.

Tests: 38 pass (12 new). GitRepoFixture spins up a real temp repo with a
seed commit; tests skip gracefully if `git` isn't on PATH.
CommitMessageBuilder fully unit-tested. WorktreeManager covers create,
no-change skip, real-commit, and non-repo failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:29:26 +02:00

78 lines
2.4 KiB
C#

using ClaudeDo.Worker.Runner;
namespace ClaudeDo.Worker.Tests.Runner;
public class CommitMessageBuilderTests
{
[Fact]
public void Slug_FromListName()
{
Assert.Equal("lager-app", CommitMessageBuilder.ToSlug("Lager App"));
}
[Fact]
public void Slug_SpecialCharsStripped()
{
Assert.Equal("my-list", CommitMessageBuilder.ToSlug("My! @List#"));
}
[Fact]
public void Slug_CollapsesDashes()
{
Assert.Equal("a-b", CommitMessageBuilder.ToSlug("a -- b"));
}
[Fact]
public void Slug_TrimsLeadingTrailingDashes()
{
Assert.Equal("abc", CommitMessageBuilder.ToSlug("--abc--"));
}
[Fact]
public void Title_TruncatedTo60()
{
var longTitle = new string('x', 80);
var msg = CommitMessageBuilder.Build("feat", "My List", longTitle, null, "task-id-123");
var header = msg.Split('\n')[0];
// Header format: feat(my-list): <60 chars>
var titlePart = header.Split(": ", 2)[1];
Assert.Equal(60, titlePart.Length);
}
[Fact]
public void NoDescription_TrailerStillPresent()
{
var msg = CommitMessageBuilder.Build("chore", "Test List", "do something", null, "abc-123");
var lines = msg.Split('\n');
// header \n \n trailer = 3 lines (no description block, just blank separator).
Assert.Equal(3, lines.Length);
Assert.Equal("chore(test-list): do something", lines[0]);
Assert.Equal("", lines[1]);
Assert.Equal("ClaudeDo-Task: abc-123", lines[2]);
}
[Fact]
public void WithDescription_IncludedAboveTrailer()
{
var msg = CommitMessageBuilder.Build("feat", "Lager App", "add scan", "Detailed description here", "id-456");
var lines = msg.Split('\n');
Assert.Equal("feat(lager-app): add scan", lines[0]);
Assert.Equal("", lines[1]); // blank after header
Assert.Equal("Detailed description here", lines[2]);
Assert.Equal("", lines[3]); // blank before trailer
Assert.Equal("ClaudeDo-Task: id-456", lines[4]);
}
[Fact]
public void Description_TruncatedTo400()
{
var longDesc = new string('d', 500);
var msg = CommitMessageBuilder.Build("fix", "X", "title", longDesc, "id");
var lines = msg.Split('\n');
// lines[2] is the description line.
Assert.Equal(400, lines[2].Length);
}
}