}),"What the AI cited comes first, then what the tools read, then what the data providers gave -- and a reader can follow the numbers straight down the list.");
}
[Test]
publicvoidATitleCannotBreakOutOfItsLink()
{
IList<Source>sources=[new("A [strange] title\\with a break\nin it","https://example.org/",SourceOrigin.TOOL)];
Assert.That(EntriesOf(sources.ToMarkdown()).Single(),Is.EqualTo(@"- [1] [A \[strange\] title\\with a break in it](<https://example.org/>)"),"Brackets and backslashes are escaped, and the line break becomes a space so the entry stays one line.");
}
[Test]
publicvoidALocalPathKeepsWorkingAsALink()
{
// This is what a RAG hit on a file of the user looks like, and spaces in file names are the
Assert.That(EntriesOf(sources.ToMarkdown()).Single(),Is.EqualTo("- [1] [Handbook (page 12)](<file:///Users/someone/My%20Documents/handbook.pdf>)"),"A space would end the link destination, so it is escaped.");
}
[Test]
publicvoidNoSourcesMeanNoText()
{
IList<Source>sources=[];
Assert.Multiple(()=>
{
Assert.That(sources.ToMarkdown(),Is.Empty);
Assert.That(sources.ToExportMarkdown(),Is.Empty,"An answer nobody had to look up gets no heading over an empty list.");
Assert.That(document.OfType<HeadingBlock>().Select(heading=>heading.Level),Is.EqualTo(new[]{1,2,2}),"One heading of its own stands above the two groups the chat already shows.");
Assert.That(exported,Does.EndWith(sources.ToMarkdown()),"Below that heading, the export is what the chat shows, unchanged.");
Assert.That(sources.GroupSources(),Has.Count.EqualTo(3),"Each of the three origins has a source, so each of them is a group.");
Assert.That(listed.Select(numbered=>numbered.Source.Title),Is.EqualTo(new[]{"Cited by the model","Search result","Handbook","Second handbook"}),"What the AI cited comes first, then what the tools read, then what the data providers gave.");
Assert.That(listed.Select(numbered=>numbered.Number),Is.EqualTo(new[]{1,2,3,4}),"The number runs through the whole list instead of starting over per group.");
Assert.That(sources.GroupSources().Select(group=>group.Sources.Count),Is.EqualTo(new[]{1}),"An answer which only used a tool gets one group, not three with two of them empty.");
Assert.That(newList<Source>().GroupSources(),Is.Empty,"An answer nobody had to look up gets no group at all.");
Assert.That(entries,Has.Count.EqualTo(listed.Count),"Every source the grouping lists is written out, and nothing else is.");
for(varindex=0;index<entries.Count;index++)
Assert.That(entries[index],Does.StartWith($"- [{listed[index].Number}] ").And.Contains(listed[index].Source.Title),"The Markdown and the chat read the same grouping, so a source cannot be numbered one way here and another way there.");
}),"Word and LibreOffice take the fragment of a local link for part of the file name and refuse the link, so the local links lose it -- and the web link keeps its own, where a fragment names a section of the page and belongs to the address.");
Assert.That(EntriesOf(sources.ToMarkdown()).Single(),Does.EndWith("handbook.pdf#page=266>)"),"A browser and a PDF reader open the document where the passage is, so they are told the page.");
Assert.That(EntriesOf(sources.ToExportMarkdown()).Single(),Does.EndWith("handbook.pdf#page=266>)"),"The clipboard and every text format keep it as well; only the two office formats ask for it to be dropped.");
Assert.That(location.Path,Does.EndWith("Größere Übersicht.pdf").And.Contains("My Documents"),"The percent-encoding of the link is undone, so the program is handed the name the file really has.");
Assert.That(location.PageNumber,Is.EqualTo(12),"This is the page the passage was found on, and the page the document is opened at.");
Assert.That(location.Path,Is.EqualTo(@"C:\Users\someone\Documents\handbook.pdf"),"A drive letter and backslashes are what a program on Windows is handed -- and what the link was made from there.");
Assert.That(LocationOf("file:///Users/someone/handbook.pdf#chunk=3&page=12").PageNumber,Is.EqualTo(12),"A link which already carried a fragment gets the page appended with an ampersand, and it is found there too.");
});
}
[Test]
publicvoidAWebSourceNamesNoDocumentAtAll()
{
// The fragment reads like a page on purpose: what decides is the scheme, not the fragment.