Remove Scenes, group beats by multiple characters; strip comments repo-wide

Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
This commit is contained in:
James Wampler
2026-08-11 21:05:13 -07:00
parent 1ce526019f
commit 23348327a9
57 changed files with 2600 additions and 1421 deletions
@@ -10,9 +10,6 @@ public class AnthropicClientTests
{
[Test]
public void Constructing_without_a_key_does_not_throw() =>
// The agent service takes this as a dependency and also serves read-only endpoints
// (listing conversations, reading a transcript). Throwing at construction would
// take those down on any install that has not configured a key yet.
Assert.That(
() => new AnthropicAgentModelClient(Options.Create(new AgentOptions()), NullLogger<AnthropicAgentModelClient>.Instance),
Throws.Nothing);
+11 -52
View File
@@ -1,9 +1,7 @@
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Scenes;
namespace Novelly.Api.Tests;
@@ -75,22 +73,20 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task A_beat_resolves_its_character_and_scene_names()
public async Task A_beat_can_carry_several_characters()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas",
CharacterId: ines.Id,
CharacterIds: [ines.Id, mara.Id],
WhatHappened: "The pages go up faster than she expected.",
WhatsNext: "Nothing to navigate by but memory.",
SceneId: scene.Id));
WhatsNext: "Nothing to navigate by but memory."));
Assert.Multiple(() =>
{
Assert.That(beat.Character!.Name, Is.EqualTo("Ines"));
Assert.That(beat.Scene!.Title, Is.EqualTo("The dock at dawn"));
Assert.That(beat.Characters.Select(c => c.Name), Is.EquivalentTo(new[] { "Ines", "Mara" }));
Assert.That(beat.WhatHappened, Does.Contain("faster than she expected"));
});
}
@@ -103,41 +99,10 @@ public class BeatServiceTests : ServiceTestFixture
Assert.That(
async () => await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("A beat", CharacterId: stranger.Id)),
_chapterId, new CreateBeatRequest("A beat", CharacterIds: [stranger.Id])),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same project"));
}
[Test]
public async Task A_beat_cannot_be_grouped_under_a_scene_from_another_chapter()
{
var elsewhere = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Elsewhere"));
var scene = await Scenes.CreateAsync(elsewhere.Id, new CreateSceneRequest("Another scene"));
Assert.That(
async () => await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("A beat", SceneId: scene.Id)),
Throws.TypeOf<InvalidOperationException>().With.Message.Contains("same chapter"));
}
[Test]
public async Task Deleting_a_scene_leaves_its_beats_alone()
{
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var beat = await Beats.CreateAsync(
_chapterId, new CreateBeatRequest("She burns the atlas", SceneId: scene.Id));
await Scenes.DeleteAsync(scene.Id);
// The plan outlives a decision about prose — the beat is simply ungrouped.
var survivor = (await Beats.GetAsync(beat.Id))!;
Assert.Multiple(() =>
{
Assert.That(survivor.SceneId, Is.Null);
Assert.That(survivor.Title, Is.EqualTo("She burns the atlas"));
});
}
[Test]
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
{
@@ -164,23 +129,17 @@ public class BeatServiceTests : ServiceTestFixture
}
[Test]
public async Task ClearCharacter_and_ClearScene_detach_the_reference_since_a_null_id_means_leave_it_alone()
public async Task An_empty_CharacterIds_list_clears_a_beats_characters_since_null_means_leave_it_alone()
{
var ines = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Ines"));
var scene = await Scenes.CreateAsync(_chapterId, new CreateSceneRequest("The dock at dawn"));
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest(
"She burns the atlas", CharacterId: ines.Id, SceneId: scene.Id));
"She burns the atlas", CharacterIds: [ines.Id]));
var untouched = (await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(Title: "She burns it")))!;
Assert.That(untouched.Character!.Name, Is.EqualTo("Ines"));
Assert.That(untouched.Characters.Select(c => c.Name), Is.EqualTo(new[] { "Ines" }));
var cleared = (await Beats.UpdateAsync(
beat.Id, new UpdateBeatRequest(ClearCharacter: true, ClearScene: true)))!;
var cleared = (await Beats.UpdateAsync(beat.Id, new UpdateBeatRequest(CharacterIds: [])))!;
Assert.Multiple(() =>
{
Assert.That(cleared.Character, Is.Null);
Assert.That(cleared.Scene, Is.Null);
});
Assert.That(cleared.Characters, Is.Empty);
}
}
@@ -2,14 +2,8 @@ using Microsoft.Extensions.Logging;
namespace Novelly.Api.Tests;
/// <summary>Records every entry logged through it, so tests can assert on what a service logged.</summary>
public record CapturedLogEntry(LogLevel Level, string Message, Exception? Exception);
/// <summary>
/// A test double for <see cref="ILogger{TCategoryName}"/> that captures entries instead of
/// writing them anywhere, so tests can assert a service logged at the right level with the
/// right values without standing up a real sink.
/// </summary>
public class CapturingLogger<T> : ILogger<T>
{
public List<CapturedLogEntry> Entries { get; } = [];
+3 -6
View File
@@ -64,8 +64,6 @@ public class CharacterArcTests : ServiceTestFixture
[Test]
public async Task Within_a_group_the_lead_comes_before_the_second_lead()
{
// Both enums are stored as text, so ordering them in SQL orders the spelling and
// "Deuteragonist" beats "Protagonist" — burying the character the book is about.
await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
"Mara", CharacterRole.Deuteragonist, CharacterImportance.Main));
await Characters.CreateAsync(_projectId, new CreateCharacterRequest(
@@ -162,7 +160,6 @@ public class CharacterArcTests : ServiceTestFixture
await Chapters.DeleteAsync(chapter.Id);
// How a character changes outlives a decision about where the chapter break falls.
var survivor = (await Arcs.GetAsync(stage.Id))!;
Assert.Multiple(() =>
@@ -195,9 +192,9 @@ public class CharacterArcTests : ServiceTestFixture
var first = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("First", Number: 1));
var mara = await Characters.CreateAsync(_projectId, new CreateCharacterRequest("Mara"));
await Beats.CreateAsync(second.Id, new CreateBeatRequest("She boards anyway", CharacterId: _characterId));
await Beats.CreateAsync(first.Id, new CreateBeatRequest("She finds the map", CharacterId: _characterId));
await Beats.CreateAsync(first.Id, new CreateBeatRequest("Mara lies", CharacterId: mara.Id));
await Beats.CreateAsync(second.Id, new CreateBeatRequest("She boards anyway", CharacterIds: [_characterId]));
await Beats.CreateAsync(first.Id, new CreateBeatRequest("She finds the map", CharacterIds: [_characterId]));
await Beats.CreateAsync(first.Id, new CreateBeatRequest("Mara lies", CharacterIds: [mara.Id]));
await Beats.CreateAsync(first.Id, new CreateBeatRequest("Nobody's beat"));
var beats = (await Beats.ListForCharacterAsync(_characterId))!;
@@ -5,11 +5,6 @@ using Novelly.Api.Projects;
namespace Novelly.Api.Tests;
/// <summary>
/// Covers the exception-handling rework: a missing entity is an ordinary result, not a
/// thrown exception; <see cref="Guard"/> rejects missing required arguments; and a
/// service re-validates a request even when a direct caller skips the API's own filter.
/// </summary>
[TestFixture]
public class ExceptionHandlingTests : ServiceTestFixture
{
@@ -64,8 +64,6 @@ public class ImportAgentToolsetTests : ServiceTestFixture
[Test]
public async Task Write_ledger_can_only_ever_touch_the_ledger_file_no_matter_what_path_is_asked_for()
{
// The tool takes no path argument at all — this is the enforcement, not a check
// against a supplied path. Confirm the write always lands at exactly the ledger name.
await _toolset.ExecuteAsync("write_ledger", Input(new { json = """{"completedPasses": ["project"]}""" }));
Assert.Multiple(() =>
@@ -105,7 +105,6 @@ public class ImportServiceTests : ServiceTestFixture
Assert.That(second.Id, Is.EqualTo(first.Id));
// Only one job was ever queued.
Assert.That(_queue.Reader.TryRead(out _), Is.True);
Assert.That(_queue.Reader.TryRead(out _), Is.False);
}
+7 -19
View File
@@ -4,15 +4,9 @@ using Novelly.Api.Agent;
using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Scenes;
namespace Novelly.Api.Tests;
/// <summary>
/// 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.
/// </summary>
[TestFixture]
public class ListingTests : ServiceTestFixture
{
@@ -22,7 +16,6 @@ public class ListingTests : ServiceTestFixture
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();
@@ -41,10 +34,8 @@ public class ListingTests : ServiceTestFixture
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"));
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall", Prose: "One two three"));
await Chapters.CreateAsync(project.Id, new CreateChapterRequest("The Harbour", Prose: "Four five"));
var summary = (await Projects.ListAsync()).Single();
@@ -71,22 +62,19 @@ public class ListingTests : ServiceTestFixture
}
[Test]
public async Task Chapters_are_listed_in_manuscript_order_with_scene_totals()
public async Task Chapters_are_listed_in_manuscript_order_with_word_counts()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var second = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Second", Number: 2));
var second = await Chapters.CreateAsync(project.Id, new CreateChapterRequest("Second", Number: 2, Prose: "One two three"));
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);
Assert.Multiple(() =>
{
Assert.That(listed.Select(c => c.Title), Is.EqualTo(new[] { "First", "Second" }));
Assert.That(listed.Single(c => c.Id == second.Id).Scenes, Has.Count.EqualTo(2));
Assert.That(listed.Single(c => c.Id == second.Id).Scenes.Sum(s => s.WordCount), Is.EqualTo(3));
Assert.That(listed.Single(c => c.Id == first.Id).Scenes.Sum(s => s.WordCount), Is.EqualTo(0));
Assert.That(listed.Single(c => c.Id == second.Id).WordCount, Is.EqualTo(3));
Assert.That(listed.Single(c => c.Id == first.Id).WordCount, Is.EqualTo(0));
});
}
@@ -97,7 +85,7 @@ public class ListingTests : ServiceTestFixture
var agent = new NovelAgentService(
Db.Context,
new ScriptedModelClient([[new AgentTextBlock("Reply.")]]),
new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Scenes, Tags, Questions, NullLogger<NovelAgentToolset>.Instance),
new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance),
Options.Create(new AgentOptions()),
NullLogger<NovelAgentService>.Instance,
new SendAgentMessageRequestValidator());
+1 -5
View File
@@ -7,10 +7,6 @@ using Novelly.Api.Projects;
namespace Novelly.Api.Tests;
/// <summary>
/// Covers the logging behaviour added across the services: a warning fires before a
/// not-found is thrown, and prose bodies never leak into a log message.
/// </summary>
[TestFixture]
public class LoggingTests : ServiceTestFixture
{
@@ -82,7 +78,7 @@ public class LoggingTests : ServiceTestFixture
BeatLogs.Entries.Clear();
Assert.That(
() => Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Arrival", CharacterId: foreignCharacter.Id)),
() => Beats.CreateAsync(chapter.Id, new CreateBeatRequest("Arrival", CharacterIds: [foreignCharacter.Id])),
Throws.TypeOf<InvalidOperationException>());
Assert.Multiple(() =>
@@ -12,7 +12,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
private NovelAgentToolset _toolset = null!;
protected override void OnSetUp() =>
_toolset = new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Scenes, Tags, Questions, NullLogger<NovelAgentToolset>.Instance);
_toolset = new NovelAgentToolset(Projects, Characters, Arcs, Chapters, Beats, Tags, Questions, NullLogger<NovelAgentToolset>.Instance);
private NovelAgentService BuildAgent(ScriptedModelClient model) => new(
Db.Context,
@@ -143,7 +143,6 @@ public class NovelAgentServiceTests : ServiceTestFixture
{
var projectId = (await Projects.CreateAsync(new CreateProjectRequest("The Salt Road"))).Id;
// A model that only ever asks for more tools would otherwise loop forever.
var model = new ScriptedModelClient(
Enumerable.Repeat<IReadOnlyList<AgentContentBlock>>(
[ToolUse("t", "list_characters", new { })], 20).ToList());
@@ -178,7 +177,6 @@ public class NovelAgentServiceTests : ServiceTestFixture
{
Assert.That(second.ConversationId, Is.EqualTo(first.ConversationId));
// The second request replays the earlier turns so the model has the history.
Assert.That(model.Transcripts[1], Has.Count.EqualTo(3));
Assert.That(
model.Transcripts[1].Select(m => m.Role),
@@ -209,10 +207,6 @@ public class NovelAgentServiceTests : ServiceTestFixture
new(id, name, JsonSerializer.SerializeToElement(input));
}
/// <summary>
/// A model stand-in that returns a fixed script of turns and records every transcript it
/// was sent, so tests can assert on what the loop actually put in front of the model.
/// </summary>
internal class ScriptedModelClient(IReadOnlyList<IReadOnlyList<AgentContentBlock>> script)
: IAgentModelClient
{
@@ -88,7 +88,6 @@ public class OpenQuestionTests : ServiceTestFixture
Assert.That(open.Select(q => q.Question), Is.EqualTo(new[] { "Is this one book or two?" }));
Assert.That(everything, Has.Count.EqualTo(2));
// Still-open questions come first, so the list stays about what is undecided.
Assert.That(everything[0].IsResolved, Is.False);
Assert.That(everything[1].IsResolved, Is.True);
});
@@ -128,7 +127,6 @@ public class OpenQuestionTests : ServiceTestFixture
Assert.Multiple(() =>
{
// The existing note is kept and the decision lands underneath it.
Assert.That(chapter.Notes, Does.StartWith("Runs long."));
Assert.That(chapter.Notes, Does.Contain("Where does the chapter break? — After the harbour burns."));
Assert.That(character.Notes, Is.EqualTo("Where does the chapter break? — After the harbour burns."));
@@ -179,7 +177,6 @@ public class OpenQuestionTests : ServiceTestFixture
{
Assert.That(detached.ChapterId, Is.Null);
// Only the chapter was cleared — a null id means "leave alone", not "detach".
Assert.That(detached.CharacterId, Is.EqualTo(_characterId));
});
}
+11 -16
View File
@@ -3,7 +3,6 @@ using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Common;
using Novelly.Api.Projects;
using Novelly.Api.Scenes;
namespace Novelly.Api.Tests;
@@ -69,19 +68,18 @@ public class ProjectDataTests : ServiceTestFixture
public async Task Word_count_is_recomputed_whenever_prose_changes()
{
var projectId = await NewProjectAsync();
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
"The dock at dawn", Prose: "Five words go right here"));
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest(
"Landfall", Prose: "Five words go right here"));
Assert.That(scene.WordCount, Is.EqualTo(5));
Assert.That(chapter.WordCount, Is.EqualTo(5));
var rewritten = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(
var rewritten = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(
Prose: "Now\nthere are seven words in total")))!;
Assert.That(rewritten.WordCount, Is.EqualTo(7));
var cleared = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Prose: "")))!;
var cleared = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Prose: "")))!;
Assert.Multiple(() =>
{
@@ -91,14 +89,13 @@ public class ProjectDataTests : ServiceTestFixture
}
[Test]
public async Task Scene_updates_that_omit_prose_leave_the_draft_untouched()
public async Task Chapter_updates_that_omit_prose_leave_the_draft_untouched()
{
var projectId = await NewProjectAsync();
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
var scene = await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest(
"The dock at dawn", Prose: "The tide came in slow."));
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest(
"Landfall", Prose: "The tide came in slow."));
var updated = (await Scenes.UpdateAsync(scene.Id, new UpdateSceneRequest(Status: DraftStatus.Revised)))!;
var updated = (await Chapters.UpdateAsync(chapter.Id, new UpdateChapterRequest(Status: DraftStatus.Revised)))!;
Assert.Multiple(() =>
{
@@ -109,12 +106,11 @@ public class ProjectDataTests : ServiceTestFixture
}
[Test]
public async Task Deleting_a_project_takes_its_characters_chapters_and_scenes()
public async Task Deleting_a_project_takes_its_characters_and_chapters()
{
var projectId = await NewProjectAsync();
await Characters.CreateAsync(projectId, new CreateCharacterRequest("Ines"));
var chapter = await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
await Scenes.CreateAsync(chapter.Id, new CreateSceneRequest("The dock at dawn"));
await Chapters.CreateAsync(projectId, new CreateChapterRequest("Landfall"));
await Projects.DeleteAsync(projectId);
@@ -125,7 +121,6 @@ public class ProjectDataTests : ServiceTestFixture
Assert.That(await verification.Projects.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Characters.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Chapters.CountAsync(), Is.EqualTo(0));
Assert.That(await verification.Scenes.CountAsync(), Is.EqualTo(0));
});
}
@@ -3,20 +3,10 @@ using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Projects;
using Novelly.Api.Questions;
using Novelly.Api.Scenes;
using Novelly.Api.Tags;
namespace Novelly.Api.Tests;
/// <summary>
/// Shared plumbing for the service tests: a fresh in-memory database and a matching set
/// of services per test.
/// </summary>
/// <remarks>
/// The setup lives in <c>[SetUp]</c> rather than a constructor or field initialisers
/// because NUnit builds one fixture instance for the whole class — anything created once
/// would leak state from one test into the next.
/// </remarks>
public abstract class ServiceTestFixture
{
protected TestDatabase Db { get; private set; } = null!;
@@ -24,7 +14,6 @@ public abstract class ServiceTestFixture
protected ProjectService Projects { get; private set; } = null!;
protected CharacterService Characters { get; private set; } = null!;
protected ChapterService Chapters { get; private set; } = null!;
protected SceneService Scenes { get; private set; } = null!;
protected BeatService Beats { get; private set; } = null!;
protected CharacterArcService Arcs { get; private set; } = null!;
protected OpenQuestionService Questions { get; private set; } = null!;
@@ -32,7 +21,6 @@ public abstract class ServiceTestFixture
protected CapturingLogger<ProjectService> ProjectLogs { get; private set; } = null!;
protected CapturingLogger<CharacterService> CharacterLogs { get; private set; } = null!;
protected CapturingLogger<ChapterService> ChapterLogs { get; private set; } = null!;
protected CapturingLogger<SceneService> SceneLogs { get; private set; } = null!;
protected CapturingLogger<BeatService> BeatLogs { get; private set; } = null!;
protected CapturingLogger<TagService> TagLogs { get; private set; } = null!;
protected CapturingLogger<CharacterArcService> ArcLogs { get; private set; } = null!;
@@ -47,7 +35,6 @@ public abstract class ServiceTestFixture
ProjectLogs = new CapturingLogger<ProjectService>();
CharacterLogs = new CapturingLogger<CharacterService>();
ChapterLogs = new CapturingLogger<ChapterService>();
SceneLogs = new CapturingLogger<SceneService>();
BeatLogs = new CapturingLogger<BeatService>();
ArcLogs = new CapturingLogger<CharacterArcService>();
QuestionLogs = new CapturingLogger<OpenQuestionService>();
@@ -58,7 +45,6 @@ public abstract class ServiceTestFixture
Db.Context, Tags, CharacterLogs,
new CreateCharacterRequestValidator(), new UpdateCharacterRequestValidator(), new CreateRelationshipRequestValidator());
Chapters = new ChapterService(Db.Context, Tags, ChapterLogs, new CreateChapterRequestValidator(), new UpdateChapterRequestValidator());
Scenes = new SceneService(Db.Context, SceneLogs, new CreateSceneRequestValidator(), new UpdateSceneRequestValidator());
Beats = new BeatService(
Db.Context, Tags, BeatLogs,
new CreateBeatRequestValidator(), new UpdateBeatRequestValidator(), new ReorderBeatsRequestValidator());
@@ -72,7 +58,6 @@ public abstract class ServiceTestFixture
OnSetUp();
}
/// <summary>Runs after the services exist, for per-class seed data.</summary>
protected virtual void OnSetUp()
{
}
-11
View File
@@ -4,16 +4,6 @@ using Novelly.Api.Data;
namespace Novelly.Api.Tests;
/// <summary>
/// A throwaway SQLite database held in memory. Using real SQLite rather than the
/// in-memory provider means the tests exercise the same relational behaviour the app
/// ships with — cascade deletes, foreign keys and all.
/// </summary>
/// <remarks>
/// NUnit reuses one fixture instance across every test in a class, so this must be built
/// in <c>[SetUp]</c> and disposed in <c>[TearDown]</c>. A field initialiser would share
/// one database for the whole class and let tests see each other's rows.
/// </remarks>
public class TestDatabase : IDisposable
{
private readonly SqliteConnection _connection;
@@ -29,7 +19,6 @@ public class TestDatabase : IDisposable
public NovelDbContext Context { get; }
/// <summary>A second context over the same database, for asserting on persisted state.</summary>
public NovelDbContext CreateContext() =>
new(new DbContextOptionsBuilder<NovelDbContext>().UseSqlite(_connection).Options);