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>
89 lines
4.5 KiB
C#
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("\n", text);
|
|
}
|
|
}
|