Files
Wiki/test/Wiki.Application.Tests/RecordStatementUseCaseTests.cs
T
claude-botandClaude Fable 5.1 c94afb004c
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
feat(facts): pictures as part of a statement, kept next to the raw source
A statement may carry images (a type label, for instance). The drafter
sees them as image blocks and transcribes what is printed; the deposit
stores each picture beside the note as raw/<stem>-<n>.<ext>, lists it in
the frontmatter and embeds it in the body. INoteStore gains a binary
create and a browse URL, so the raw page can link to the pictures. The
page takes pictures from a file picker (camera on phones) or from paste,
resized in the browser to 1600px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 09:52:16 +02:00

89 lines
4.5 KiB
C#

using Microsoft.Extensions.Time.Testing;
using Wiki.Application.Facts;
using Wiki.Core.Model;
using Wiki.Core.Ports;
namespace Wiki.Application.Tests;
public sealed class FakeFactDrafter : IFactDrafter
{
public List<FactStatement> Statements { get; } = [];
public FactDraft Draft { get; set; } = new("Fußbodenheizung im Erdgeschoss", "Alle Räume im Erdgeschoss haben Fußbodenheizung; die Garderobe hat keine.", ["heating", "ground-floor"]);
public string ModelId => "fake-drafter";
public string Name => "fake-drafter";
public Task<FactDraft> DraftAsync(FactStatement statement, CancellationToken ct)
{
Statements.Add(statement);
return Task.FromResult(Draft);
}
public Task<AdapterHealth> CheckAsync(CancellationToken ct) => Task.FromResult(new AdapterHealth(Name, true, null, TimeSpan.Zero, DateTimeOffset.UnixEpoch));
}
public class RecordStatementUseCaseTests
{
private static readonly DateTimeOffset Now = new(2026, 9, 25, 8, 0, 0, TimeSpan.Zero);
private static readonly Identity By = new("k.renner", IdentityKind.Human);
private const string Statement = "Alle Räume im Erdgeschoss haben Fußbodenheizung außer die Garderobe";
[Fact]
public async Task StatementBecomesARawSourceUnderThePersonWithTheVerbatimText()
{
var store = new FakeStore();
var drafter = new FakeFactDrafter();
var useCase = new RecordStatementUseCase(new DepositFactUseCase(store, new FakeTimeProvider(Now)), new FakeTimeProvider(Now));
var recorded = await useCase.RecordAsync(" " + Statement + "\n", [], By, drafter, CancellationToken.None);
var sent = Assert.Single(drafter.Statements);
Assert.Equal(Statement, sent.Text);
Assert.Equal(By, sent.By);
Assert.Equal("raw/2026-09-25-fu-bodenheizung-im-erdgeschoss.md", recorded.Receipt.Path);
var text = store.Files[recorded.Receipt.Path];
Assert.Contains("title: Fußbodenheizung im Erdgeschoss\n", text);
Assert.Contains("source: stated by k.renner on 2026-09-25 via the wiki\n", text);
Assert.Contains("captured_by: k.renner\n", text);
Assert.Contains("tags: [heating, ground-floor]\n", text);
Assert.Contains("die Garderobe hat keine.\n\nStatement, verbatim:\n\n> " + Statement + "\n", text);
}
[Fact]
public async Task ADraftThatIsNoRawSourceIsNotStored()
{
var store = new FakeStore();
var drafter = new FakeFactDrafter { Draft = new FactDraft(" ", "the verbatim statement always makes the body long enough; only a missing title fails", []) };
var useCase = new RecordStatementUseCase(new DepositFactUseCase(store, new FakeTimeProvider(Now)), new FakeTimeProvider(Now));
await Assert.ThrowsAsync<InvalidOperationException>(() => useCase.RecordAsync(Statement, [], By, drafter, CancellationToken.None));
Assert.Empty(store.Files);
}
[Fact]
public async Task EmptyStatementsNeverReachTheDrafter()
{
var drafter = new FakeFactDrafter();
var useCase = new RecordStatementUseCase(new DepositFactUseCase(new FakeStore(), new FakeTimeProvider(Now)), new FakeTimeProvider(Now));
await Assert.ThrowsAsync<ArgumentException>(() => useCase.RecordAsync(" \n", [], By, drafter, CancellationToken.None));
Assert.Empty(drafter.Statements);
}
[Fact]
public async Task PicturesReachTheDrafterAndLandNextToTheNote()
{
var store = new FakeStore();
var drafter = new FakeFactDrafter { Draft = new FactDraft("Geschirrspüler Typenschild", "Der Geschirrspüler ist ein Bosch SMV4HCX48E, Seriennummer [unreadable].", ["dishwasher"]) };
var useCase = new RecordStatementUseCase(new DepositFactUseCase(store, new FakeTimeProvider(Now)), new FakeTimeProvider(Now));
var photo = new Attachment("image/jpeg", [0xFF, 0xD8, 0xFF, 1, 2, 3], "type label");
var recorded = await useCase.RecordAsync("Das ist das Typenschild unseres Geschirrspülers.", [photo], By, drafter, CancellationToken.None);
Assert.Same(photo, Assert.Single(Assert.Single(drafter.Statements).Attachments));
Assert.Equal("raw/2026-09-25-geschirrsp-ler-typenschild.md", recorded.Receipt.Path);
Assert.Equal(photo.Bytes, store.Binaries["raw/2026-09-25-geschirrsp-ler-typenschild-1.jpg"]);
var text = store.Files[recorded.Receipt.Path];
Assert.Contains("attachments: [2026-09-25-geschirrsp-ler-typenschild-1.jpg]\n", text);
Assert.EndsWith("![type label](2026-09-25-geschirrsp-ler-typenschild-1.jpg)\n", text);
}
}