using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NovelSoftware.Application.Agent; using NovelSoftware.Application.Dtos; using NovelSoftware.Application.Services; namespace NovelSoftware.Tests; /// /// Covers the list endpoints, which sort and aggregate in SQL rather than in memory. /// SQLite is fussier than the in-memory provider about what it will translate — ordering /// by a DateTimeOffset, for one — so these have to run against real SQLite to be worth anything. /// public class ListingTests : IDisposable { private readonly TestDatabase _db = new(); private readonly TagService _tags; private readonly ProjectService _projects; private readonly ChapterService _chapters; private readonly SceneService _scenes; private readonly CharacterService _characters; public ListingTests() { _tags = new TagService(_db.Context); _projects = new ProjectService(_db.Context); _chapters = new ChapterService(_db.Context, _tags); _scenes = new SceneService(_db.Context); _characters = new CharacterService(_db.Context, _tags); } [Fact] public async Task Projects_are_listed_most_recently_updated_first() { var older = await _projects.CreateAsync(new CreateProjectRequest("Older Book")); var newer = await _projects.CreateAsync(new CreateProjectRequest("Newer Book")); // Touching the older project should float it to the top. await _projects.UpdateAsync(older.Id, new UpdateProjectRequest(Logline: "Revised.")); var listed = await _projects.ListAsync(); listed.Select(p => p.Title).Should().Equal("Older Book", "Newer Book"); listed.Select(p => p.Id).Should().Contain(newer.Id); } [Fact] public async Task Project_summaries_aggregate_counts_and_words_across_chapters() { var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road")); await _characters.CreateAsync(project.Id, new CreateCharacterRequest("Ines")); await _characters.CreateAsync(project.Id, new CreateCharacterRequest("Mara")); var first = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall")); var second = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("The Harbour")); await _scenes.CreateAsync(first.Id, new CreateSceneRequest("Dawn", Prose: "One two three")); await _scenes.CreateAsync(second.Id, new CreateSceneRequest("Dusk", Prose: "Four five")); var summary = (await _projects.ListAsync()).Single(); summary.CharacterCount.Should().Be(2); summary.ChapterCount.Should().Be(2); summary.WordCount.Should().Be(5); } [Fact] public async Task A_project_with_no_chapters_reports_zero_words_rather_than_failing() { await _projects.CreateAsync(new CreateProjectRequest("Empty")); var summary = (await _projects.ListAsync()).Single(); summary.WordCount.Should().Be(0); summary.ChapterCount.Should().Be(0); } [Fact] public async Task Chapters_are_listed_in_manuscript_order_with_scene_totals() { var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road")); var second = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("Second", Number: 2)); var first = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("First", Number: 1)); await _scenes.CreateAsync(second.Id, new CreateSceneRequest("A", Prose: "One two")); await _scenes.CreateAsync(second.Id, new CreateSceneRequest("B", Prose: "Three")); var listed = await _chapters.ListAsync(project.Id); listed.Select(c => c.Title).Should().Equal("First", "Second"); listed.Single(c => c.Id == second.Id).SceneCount.Should().Be(2); listed.Single(c => c.Id == second.Id).WordCount.Should().Be(3); listed.Single(c => c.Id == first.Id).WordCount.Should().Be(0); } [Fact] public async Task Conversations_are_listed_most_recently_updated_first() { var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road")); var agent = new NovelAgentService( _db.Context, new ScriptedModelClient([[new AgentTextBlock("Reply.")]]), new NovelAgentToolset(_projects, _characters, _chapters, new BeatService(_db.Context, _tags), _scenes, _tags), Options.Create(new AgentOptions()), NullLogger.Instance); await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("First question.")); await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("Second question.")); var listed = await agent.ListConversationsAsync(project.Id); listed.Should().HaveCount(2); listed[0].Title.Should().Be("Second question."); listed[0].MessageCount.Should().Be(2); } [Fact] public async Task Characters_are_listed_by_role_then_name() { var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road")); await _characters.CreateAsync(project.Id, new CreateCharacterRequest( "Zeno", NovelSoftware.Domain.CharacterRole.Supporting)); await _characters.CreateAsync(project.Id, new CreateCharacterRequest( "Ines", NovelSoftware.Domain.CharacterRole.Protagonist)); await _characters.CreateAsync(project.Id, new CreateCharacterRequest( "Anders", NovelSoftware.Domain.CharacterRole.Supporting)); var listed = await _characters.ListAsync(project.Id); listed.Select(c => c.Name).Should().Equal("Ines", "Anders", "Zeno"); } public void Dispose() => _db.Dispose(); }