using ClaudeDo.Worker.Findings; namespace ClaudeDo.Worker.Tests.Findings; public sealed class FindingsStoreTests : IDisposable { private readonly string _root; public FindingsStoreTests() { _root = Path.Combine(Path.GetTempPath(), "cdo-findings-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_root); } public void Dispose() { try { Directory.Delete(_root, recursive: true); } catch { /* best effort */ } } private static FindingInput Input(string slug, string title = "Something is not what it looks like") => new(slug, title, "Body line one.\n\nBody line two.", "src/ClaudeDo.Worker", "task-1", "abc1234"); [Fact] public async Task SaveAsync_WritesFindingFileWithFrontmatter() { var store = new FindingsStore(); var outcome = await store.SaveAsync(_root, Input("conpty-arg-quoting"), CancellationToken.None); Assert.True(outcome.Created); Assert.Equal("conpty-arg-quoting", outcome.Slug); var text = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "traps", "conpty-arg-quoting.md")); Assert.StartsWith("---\n", text.Replace("\r\n", "\n")); Assert.Contains("slug: conpty-arg-quoting", text); Assert.Contains("scope: src/ClaudeDo.Worker", text); Assert.Contains("source-task: task-1", text); Assert.Contains("verified-against: abc1234", text); Assert.Contains("# Something is not what it looks like", text); Assert.Contains("Body line one.", text); } [Fact] public async Task SaveAsync_SameSlugOverwritesInsteadOfCreatingASecondFile() { var store = new FindingsStore(); await store.SaveAsync(_root, Input("dup", "First title"), CancellationToken.None); var outcome = await store.SaveAsync(_root, Input("dup", "Second title"), CancellationToken.None); Assert.False(outcome.Created); Assert.Equal(1, outcome.TotalFindings); var files = Directory.GetFiles(Path.Combine(_root, ".claudedo", "traps")); Assert.Single(files); var text = await File.ReadAllTextAsync(files[0]); Assert.Contains("# Second title", text); Assert.DoesNotContain("# First title", text); } [Fact] 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); var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md")); var lines = index.Replace("\r\n", "\n").Split('\n'); var alphaAt = Array.FindIndex(lines, l => l.Contains("alpha")); var zuluAt = Array.FindIndex(lines, l => l.Contains("zulu")); Assert.True(alphaAt >= 0 && zuluAt >= 0); Assert.True(alphaAt < zuluAt); Assert.Contains("- [alpha](traps/alpha.md) — Alpha trap", index); } [Fact] 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); File.Delete(Path.Combine(_root, ".claudedo", "traps", "gone.md")); await store.SaveAsync(_root, Input("stays", "Stays trap"), CancellationToken.None); var index = await File.ReadAllTextAsync(Path.Combine(_root, ".claudedo", "INDEX.md")); Assert.DoesNotContain("gone", index); Assert.Contains("stays", index); } [Theory] [InlineData("../escape")] [InlineData("has space")] [InlineData("UPPER")] [InlineData("trailing-")] [InlineData("")] public async Task SaveAsync_RejectsInvalidSlug(string slug) { var store = new FindingsStore(); await Assert.ThrowsAsync( () => store.SaveAsync(_root, Input(slug), CancellationToken.None)); } [Fact] public async Task SaveAsync_FlagsNearCapacityAtWarnThreshold() { 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); Assert.Equal(FindingsStore.WarnThreshold, outcome.TotalFindings); Assert.True(outcome.NearCapacity); } [Fact] public async Task SaveAsync_DoesNotFlagNearCapacityBelowThreshold() { var store = new FindingsStore(); var outcome = await store.SaveAsync(_root, Input("only-one"), CancellationToken.None); Assert.False(outcome.NearCapacity); } }