using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Data.Tests; public sealed class SessionSkillRepositoryTests : IDisposable { private readonly string _dbPath; private readonly ClaudeDoDbContext _ctx; private readonly SessionSkillRepository _repo; public SessionSkillRepositoryTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_skills_{Guid.NewGuid():N}.db"); var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={_dbPath}") .Options; _ctx = new ClaudeDoDbContext(options); _ctx.Database.EnsureCreated(); _repo = new SessionSkillRepository(_ctx); } public void Dispose() { _ctx.Dispose(); foreach (var suffix in new[] { "", "-wal", "-shm" }) try { File.Delete(_dbPath + suffix); } catch { } } private static SessionSkillEntity MakeSkill(string name, string sourceUrl = "https://example.com/repo") => new() { Name = name, SourceUrl = sourceUrl, PinnedRef = "main", Subpath = "skills/" + name, Description = "desc for " + name, AddedAt = DateTimeOffset.UtcNow, }; [Fact] public async Task UpsertAsync_then_GetAsync_roundtrips() { var skill = MakeSkill("brainstorming"); await _repo.UpsertAsync(skill); var found = await _repo.GetAsync("brainstorming"); Assert.NotNull(found); Assert.Equal(skill.SourceUrl, found!.SourceUrl); Assert.Equal(skill.PinnedRef, found.PinnedRef); Assert.Equal(skill.Subpath, found.Subpath); Assert.Equal(skill.Description, found.Description); } [Fact] public async Task UpsertAsync_updates_existing_row() { await _repo.UpsertAsync(MakeSkill("brainstorming", "https://example.com/old")); await _repo.UpsertAsync(MakeSkill("brainstorming", "https://example.com/new")); var found = await _repo.GetAsync("brainstorming"); Assert.NotNull(found); Assert.Equal("https://example.com/new", found!.SourceUrl); var all = await _repo.ListAsync(); Assert.Single(all); } [Fact] public async Task GetAsync_returns_null_when_missing() { var found = await _repo.GetAsync("nope"); Assert.Null(found); } [Fact] public async Task ListAsync_returns_all_ordered_by_name() { await _repo.UpsertAsync(MakeSkill("zeta")); await _repo.UpsertAsync(MakeSkill("alpha")); var all = await _repo.ListAsync(); Assert.Equal(2, all.Count); Assert.Equal("alpha", all[0].Name); Assert.Equal("zeta", all[1].Name); } [Fact] public async Task DeleteAsync_removes_only_matching_row() { await _repo.UpsertAsync(MakeSkill("keep")); await _repo.UpsertAsync(MakeSkill("remove")); await _repo.DeleteAsync("remove"); var all = await _repo.ListAsync(); Assert.Single(all); Assert.Equal("keep", all[0].Name); } [Fact] public async Task DeleteBySourceAsync_removes_multiple_rows_same_source() { const string source = "https://example.com/shared-repo"; await _repo.UpsertAsync(MakeSkill("skill-a", source)); await _repo.UpsertAsync(MakeSkill("skill-b", source)); await _repo.UpsertAsync(MakeSkill("skill-c", "https://example.com/other")); await _repo.DeleteBySourceAsync(source); var all = await _repo.ListAsync(); Assert.Single(all); Assert.Equal("skill-c", all[0].Name); } [Fact] public async Task ListBySourceAsync_returns_only_matching_rows() { const string source = "https://example.com/shared-repo"; await _repo.UpsertAsync(MakeSkill("skill-a", source)); await _repo.UpsertAsync(MakeSkill("skill-b", source)); await _repo.UpsertAsync(MakeSkill("skill-c", "https://example.com/other")); var result = await _repo.ListBySourceAsync(source); Assert.Equal(2, result.Count); Assert.All(result, s => Assert.Equal(source, s.SourceUrl)); } }