using System.Text.Json; using ClaudeDo.Data; namespace ClaudeDo.Data.Tests; public class PromptFilesClassifyTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), "claudedo-prompt-tests-" + Guid.NewGuid()); public void Dispose() { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } [Fact] public void Classify_missing_file_returns_Missing() { Assert.Equal(PromptFileState.Missing, PromptFiles.Classify(PromptKind.System, _root)); } [Fact] public void Classify_file_matching_current_default_returns_MatchesCurrentDefault() { Directory.CreateDirectory(_root); File.WriteAllText(PromptFiles.PathFor(PromptKind.Retry, _root), PromptFiles.DefaultFor(PromptKind.Retry)); Assert.Equal(PromptFileState.MatchesCurrentDefault, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void Classify_file_matching_a_recorded_past_default_returns_MatchesKnownPastDefault() { Directory.CreateDirectory(_root); const string oldDefaultText = "This was the bundled default a while ago."; File.WriteAllText(PromptFiles.PathFor(PromptKind.Retry, _root), oldDefaultText); // Simulate what Save() would have written back when oldDefaultText WAS the current default. var hashes = new Dictionary { [PromptKind.Retry.ToString()] = PromptFiles.HashOf(PromptFiles.Normalize(oldDefaultText)) }; File.WriteAllText(Path.Combine(_root, ".defaults.json"), JsonSerializer.Serialize(hashes)); Assert.Equal(PromptFileState.MatchesKnownPastDefault, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void Classify_file_that_diverges_with_no_recorded_hash_returns_Edited() { Directory.CreateDirectory(_root); File.WriteAllText(PromptFiles.PathFor(PromptKind.Retry, _root), "My own custom retry instructions."); Assert.Equal(PromptFileState.Edited, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void Save_with_content_equal_to_default_records_hash_so_it_classifies_as_current_default() { PromptFiles.Save(PromptKind.Retry, PromptFiles.DefaultFor(PromptKind.Retry), _root); Assert.Equal(PromptFileState.MatchesCurrentDefault, PromptFiles.Classify(PromptKind.Retry, _root)); Assert.True(File.Exists(Path.Combine(_root, ".defaults.json"))); } [Fact] public void Save_with_edited_content_does_not_record_a_hash() { PromptFiles.Save(PromptKind.Retry, "Custom retry text.", _root); Assert.Equal(PromptFileState.Edited, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void ResetToDefault_deletes_the_override_file_and_its_hash_entry() { PromptFiles.Save(PromptKind.Retry, PromptFiles.DefaultFor(PromptKind.Retry), _root); Assert.True(File.Exists(PromptFiles.PathFor(PromptKind.Retry, _root))); PromptFiles.ResetToDefault(PromptKind.Retry, _root); Assert.False(File.Exists(PromptFiles.PathFor(PromptKind.Retry, _root))); Assert.Equal(PromptFileState.Missing, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void ReconcileStaleDefaults_removes_a_file_that_only_matched_a_past_default() { Directory.CreateDirectory(_root); const string oldDefaultText = "Old bundled default text."; 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)); PromptFiles.ReconcileStaleDefaults(_root); Assert.False(File.Exists(PromptFiles.PathFor(PromptKind.Retry, _root))); } [Fact] public void ReconcileStaleDefaults_leaves_a_real_edit_untouched() { PromptFiles.Save(PromptKind.Retry, "Genuinely customized retry text.", _root); PromptFiles.ReconcileStaleDefaults(_root); Assert.True(File.Exists(PromptFiles.PathFor(PromptKind.Retry, _root))); Assert.Equal(PromptFileState.Edited, PromptFiles.Classify(PromptKind.Retry, _root)); } [Fact] public void QuarantineOrphans_moves_unknown_md_files_into_orphans_subfolder() { Directory.CreateDirectory(_root); var orphanPath = Path.Combine(_root, "agent.md"); File.WriteAllText(orphanPath, "leftover from an old naming scheme"); var moved = PromptFiles.QuarantineOrphans(_root); Assert.False(File.Exists(orphanPath)); var dest = Assert.Single(moved); Assert.True(File.Exists(dest)); Assert.Equal("leftover from an old naming scheme", File.ReadAllText(dest)); } [Fact] public void QuarantineOrphans_leaves_known_prompt_files_in_place() { PromptFiles.Save(PromptKind.Retry, "Custom retry text.", _root); var moved = PromptFiles.QuarantineOrphans(_root); Assert.Empty(moved); Assert.True(File.Exists(PromptFiles.PathFor(PromptKind.Retry, _root))); } [Fact] public void DiffAgainstDefault_shows_only_the_changed_lines() { var lines = PromptFiles.DefaultFor(PromptKind.PlanningInitial).Replace("\r\n", "\n").Split('\n'); var edited = string.Join('\n', lines) + "\nExtra custom line."; PromptFiles.Save(PromptKind.PlanningInitial, edited, _root); var diff = PromptFiles.DiffAgainstDefault(PromptKind.PlanningInitial, _root); Assert.Contains("+ Extra custom line.", diff); Assert.DoesNotContain("- {title}", diff); } }