using System.Text.Json; using ClaudeDo.Data; using ClaudeDo.Worker.Lifecycle; using Microsoft.Extensions.Logging.Abstractions; namespace ClaudeDo.Worker.Tests.Lifecycle; public sealed class PromptFileRecoveryTests { [Fact] public async Task StartAsync_ReconcilesStaleDefaultAndQuarantinesOrphan_WithoutTouchingRealEdit() { var root = Path.Combine(Path.GetTempPath(), $"claudedo_prompts_{Guid.NewGuid():N}"); try { // A file that only ever matched a now-superseded default should get reconciled away. Directory.CreateDirectory(root); const string oldDefaultText = "This used to be the bundled retry default."; File.WriteAllText(PromptFiles.PathFor(PromptKind.Retry, root), oldDefaultText); var hashes = new Dictionary { [PromptKind.Retry.ToString()] = PromptFiles.HashOf(PromptFiles.Normalize(oldDefaultText)) }; File.WriteAllText(Path.Combine(root, ".defaults.json"), JsonSerializer.Serialize(hashes)); // A genuinely edited file should survive untouched. PromptFiles.Save(PromptKind.System, "My real customization.", root); // A leftover file from a retired naming scheme should be quarantined, not deleted. var orphanPath = Path.Combine(root, "agent.md"); File.WriteAllText(orphanPath, "leftover"); var sut = new PromptFileRecovery(NullLogger.Instance, root); await sut.StartAsync(CancellationToken.None); Assert.False(File.Exists(PromptFiles.PathFor(PromptKind.Retry, root)), "Stale unedited default must be reconciled away"); Assert.True(File.Exists(PromptFiles.PathFor(PromptKind.System, root)), "Real edit must survive"); Assert.False(File.Exists(orphanPath), "Orphan must be moved out of the prompts root"); Assert.True(File.Exists(Path.Combine(root, "_orphans", "agent.md")), "Orphan must be quarantined, not deleted"); await sut.StopAsync(CancellationToken.None); // must not throw } finally { if (Directory.Exists(root)) Directory.Delete(root, recursive: true); } } }