refactor(git): share the info/exclude writer with the findings store

This commit is contained in:
mika kuns
2026-08-10 09:59:42 +02:00
parent d3c2e2e7c8
commit 4d997a5f99
6 changed files with 136 additions and 76 deletions
@@ -0,0 +1,42 @@
using ClaudeDo.Worker.Findings;
using ClaudeDo.Worker.Tests.Infrastructure;
namespace ClaudeDo.Worker.Tests.Findings;
public sealed class FindingsStoreGitTests : IDisposable
{
private readonly GitRepoFixture _repo = new();
public void Dispose() => _repo.Dispose();
private static FindingInput Input(string slug)
=> new(slug, "Something is not what it looks like", "Body.", "src", "task-1", "abc1234");
[Fact]
public async Task SaveAsync_WhenNotTracked_ExcludesStoreFromGitOnce()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var store = new FindingsStore();
await store.SaveAsync(_repo.RepoDir, Input("first"), CancellationToken.None, tracked: false);
await store.SaveAsync(_repo.RepoDir, Input("second"), CancellationToken.None, tracked: false);
var exclude = await File.ReadAllLinesAsync(Path.Combine(_repo.RepoDir, ".git", "info", "exclude"));
Assert.Single(exclude, l => l.Trim() == "/.claudedo/");
}
[Fact]
public async Task SaveAsync_WhenTracked_DoesNotTouchExclude()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var store = new FindingsStore();
await store.SaveAsync(_repo.RepoDir, Input("first"), CancellationToken.None, tracked: true);
var excludePath = Path.Combine(_repo.RepoDir, ".git", "info", "exclude");
var lines = File.Exists(excludePath) ? await File.ReadAllLinesAsync(excludePath) : Array.Empty<string>();
Assert.DoesNotContain(lines, l => l.Trim() == "/.claudedo/");
}
}
@@ -25,7 +25,7 @@ public sealed class FindingsStoreTests : IDisposable
{
var store = new FindingsStore();
var outcome = await store.SaveAsync(_root, Input("conpty-arg-quoting"), CancellationToken.None);
var outcome = await store.SaveAsync(_root, Input("conpty-arg-quoting"), CancellationToken.None, tracked: true);
Assert.True(outcome.Created);
Assert.Equal("conpty-arg-quoting", outcome.Slug);
@@ -44,9 +44,9 @@ public sealed class FindingsStoreTests : IDisposable
public async Task SaveAsync_SameSlugOverwritesInsteadOfCreatingASecondFile()
{
var store = new FindingsStore();
await store.SaveAsync(_root, Input("dup", "First title"), CancellationToken.None);
await store.SaveAsync(_root, Input("dup", "First title"), CancellationToken.None, tracked: true);
var outcome = await store.SaveAsync(_root, Input("dup", "Second title"), CancellationToken.None);
var outcome = await store.SaveAsync(_root, Input("dup", "Second title"), CancellationToken.None, tracked: true);
Assert.False(outcome.Created);
Assert.Equal(1, outcome.TotalFindings);
@@ -61,8 +61,8 @@ public sealed class FindingsStoreTests : IDisposable
public async Task SaveAsync_RebuildsIndexFromDiskSortedBySlug()
{
var store = new FindingsStore();
await store.SaveAsync(_root, Input("zulu", "Zulu trap"), CancellationToken.None);
await store.SaveAsync(_root, Input("alpha", "Alpha trap"), CancellationToken.None);
await store.SaveAsync(_root, Input("zulu", "Zulu trap"), CancellationToken.None, tracked: true);
await store.SaveAsync(_root, Input("alpha", "Alpha trap"), CancellationToken.None, tracked: true);
var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md"));
var lines = index.Replace("\r\n", "\n").Split('\n');
@@ -78,11 +78,11 @@ public sealed class FindingsStoreTests : IDisposable
public async Task SaveAsync_IndexDropsFindingsDeletedOnDisk()
{
var store = new FindingsStore();
await store.SaveAsync(_root, Input("gone", "Gone trap"), CancellationToken.None);
await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None);
await store.SaveAsync(_root, Input("gone", "Gone trap"), CancellationToken.None, tracked: true);
await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None, tracked: true);
File.Delete(Path.Combine(_root, ".claudedo", "traps", "gone.md"));
await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None);
await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None, tracked: true);
var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md"));
Assert.DoesNotContain("gone", index);
@@ -100,7 +100,7 @@ public sealed class FindingsStoreTests : IDisposable
var store = new FindingsStore();
await Assert.ThrowsAsync<ArgumentException>(
() => store.SaveAsync(_root, Input(slug), CancellationToken.None));
() => store.SaveAsync(_root, Input(slug), CancellationToken.None, tracked: true));
}
[Fact]
@@ -109,7 +109,7 @@ public sealed class FindingsStoreTests : IDisposable
var store = new FindingsStore();
SaveFindingOutcome outcome = null!;
for (var i = 0; i < FindingsStore.WarnThreshold; i++)
outcome = await store.SaveAsync(_root, Input($"trap-{i:000}"), CancellationToken.None);
outcome = await store.SaveAsync(_root, Input($"trap-{i:000}"), CancellationToken.None, tracked: true);
Assert.Equal(FindingsStore.WarnThreshold, outcome.TotalFindings);
Assert.True(outcome.NearCapacity);
@@ -120,7 +120,7 @@ public sealed class FindingsStoreTests : IDisposable
{
var store = new FindingsStore();
var outcome = await store.SaveAsync(_root, Input("only-one"), CancellationToken.None);
var outcome = await store.SaveAsync(_root, Input("only-one"), CancellationToken.None, tracked: true);
Assert.False(outcome.NearCapacity);
}