using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using Wiki.Application.Ask; using Wiki.Application.Questions; using Wiki.Application.Retrieval; using Wiki.Application.Sync; using Wiki.Core.Model; using Wiki.Core.Parsing; using Wiki.Core.Ports; namespace Wiki.Application.Tests; /// An answer model that echoes the evidence slugs, so the wiring is visible in the text. public sealed class FakeAnswerModel : IAnswerModel { public List Requests { get; } = []; public string ModelId => "fake-answer"; public string Name => "fake-answer"; public async IAsyncEnumerable AnswerAsync(AnswerRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) { Requests.Add(request); yield return new AnswerChunk.Text("Yes. "); yield return new AnswerChunk.Text(string.Join(" ", request.Evidence.Select(e => $"[[{e.Paragraph.Slug}]]"))); await Task.CompletedTask; yield return new AnswerChunk.Done(ModelId, 100, 12, TimeSpan.FromMilliseconds(3)); } public Task CheckAsync(CancellationToken ct) => Task.FromResult(new AdapterHealth(Name, true, null, TimeSpan.Zero, DateTimeOffset.UnixEpoch)); } public class QuestionUseCaseTests { private readonly FakeStore _store = new(); private readonly FakeCatalog _catalog = new(); private readonly FakeIndex _index = new(); private readonly FakeEmbeddings _model = new("m1"); private readonly FakeAnswerModel _answer = new(); private QuestionUseCase UseCase => new(new AskUseCase(new RankUseCase(_index, _catalog), _catalog, new ParagraphChunker())); private async Task SeedAsync() { _store.Files["wiki/udm-dns.md"] = "---\ntitle: UDM intercepts DNS\ntype: wiki\ntags: [dns]\n---\n\n# UDM intercepts DNS\n\nThe UDM answers every query.\n"; _store.Files["wiki/ceph-osd.md"] = "---\ntitle: Ceph OSD\ntype: wiki\n---\n\n# Ceph OSD\n\nOSD fsid.\n"; var sync = new SyncUseCase(_store, _catalog, _index, [_model], TimeProvider.System, NullLogger.Instance); await sync.RunAsync(false, CancellationToken.None); return (await _catalog.GetAsync(DocumentKind.Wiki, "udm-dns", CancellationToken.None))!; } [Fact] public async Task StreamsEvidenceThenTextThenDone() { var udm = await SeedAsync(); var events = new List(); await foreach (var e in UseCase.AnswerAsync(EmbeddingText.For(udm), new QuestionOptions(K: 2), _model, new FakeJudge(), _answer, CancellationToken.None)) events.Add(e); var evidence = Assert.IsType(events[0]); Assert.Equal("udm-dns#p1", evidence.Paragraphs[0].Paragraph.Id); Assert.InRange(evidence.Covered, 0.79, 0.81); var text = string.Concat(events.OfType().Select(t => t.Delta)); Assert.Contains("[[udm-dns]]", text); var done = Assert.IsType(events[^1]); Assert.Equal("fake-answer", done.AnswerModel); var request = Assert.Single(_answer.Requests); Assert.Equal(2, request.Evidence.Count); // both paragraphs carry probability mass in the fake judge Assert.Equal("udm-dns#p1", request.Evidence[0].Paragraph.Id); Assert.True(request.Judgements.ContainsKey("answered_1")); Assert.True(request.Judgements.ContainsKey(AskUseCase.CoveredId)); } [Fact] public async Task BelowTheGateNoAnswerModelIsCalled() { var udm = await SeedAsync(); var events = new List(); await foreach (var e in UseCase.AnswerAsync(EmbeddingText.For(udm), new QuestionOptions(K: 2, CoveredThreshold: 0.95), _model, new FakeJudge(), _answer, CancellationToken.None)) events.Add(e); var gap = Assert.IsType(Assert.Single(events)); Assert.Contains("does not cover", gap.Message); Assert.Empty(_answer.Requests); } [Fact] public async Task SavedAnswerIsAnOutputDocumentInTheSynthShape() { await SeedAsync(); var evidence = new List { new(new Paragraph("udm-dns#p1", "udm-dns", "UDM", "text"), "UDM intercepts DNS", 0.8) }; var save = new SaveAnswerUseCase(_store, new FakeTimeProvider(new DateTimeOffset(2026, 9, 24, 12, 0, 0, TimeSpan.Zero))); var written = await save.SaveAsync("Does the UDM intercept DNS?", "Yes [[udm-dns]].", evidence, 0.88, "claude-sonnet", CancellationToken.None); Assert.Equal("output/2026-09-24-does-the-udm-intercept-dns.md", written.Path); var text = _store.Files[written.Path]; Assert.Contains("type: output\n", text); Assert.Contains("question: \"Does the UDM intercept DNS?\"", text.Replace("question: Does the UDM intercept DNS?", "question: \"Does the UDM intercept DNS?\"")); Assert.Contains("sources: [udm-dns]", text); Assert.Contains("confidence: high", text); Assert.Contains("- [[udm-dns]]", text); var second = await save.SaveAsync("Does the UDM intercept DNS?", "Yes again.", evidence, 0.5, "claude-sonnet", CancellationToken.None); Assert.Equal("output/2026-09-24-does-the-udm-intercept-dns-2.md", second.Path); } }