2026-09-11 20:01:45 +02:00
using AIStudio.Models.Registry ;
using AIStudio.Provider ;
using AIStudio.Tests.Models.Corpus ;
namespace AIStudio.Tests.Models ;
/// <summary>
2026-09-12 14:12:26 +02:00
/// Holds the rules to the answers somebody decided on, over the whole corpus.
2026-09-11 20:01:45 +02:00
/// </summary>
/// <remarks>
2026-09-12 14:12:26 +02:00
/// While the old rules still stood, this was the test the rebuild was carried by: every model was
/// asked of both and the two had to agree, except where the audit had found the old answer wrong.
/// That comparison is over -- the old rules are gone, and the snapshot took over the job of noticing
/// when an answer changes.
2026-09-11 20:01:45 +02:00
///
2026-09-12 14:12:26 +02:00
/// What remains is the part no snapshot can do, because it is about intent rather than about
/// answers. The models the audit found wrong have to end up where the audit said. Every model
/// reaching the global assumption has to be one somebody let reach it, and every model somebody
/// listed there has to still be reaching it. And no name may be claimed by two rules with the same
/// right.
2026-09-11 20:01:45 +02:00
/// </remarks>
[TestFixture]
public sealed class PortingDifferenceTests
{
[Test]
2026-09-12 09:41:39 +02:00
public void EveryModelTheAuditFoundWrongIsNowAnsweredTheWayItShouldBe ()
2026-09-11 20:01:45 +02:00
{
2026-09-12 09:41:39 +02:00
var corrected = ExpectedChanges . ENTRIES . Where ( change => IsAnswered ( change . Provider , change . ModelId )). ToList ();
2026-09-11 20:01:45 +02:00
Assert . Multiple (() =>
{
2026-09-12 09:41:39 +02:00
Assert . That ( corrected , Is . Not . Empty , "Nothing the audit found wrong is answered by a rule at all, which would make this test green for the wrong reason." );
2026-09-11 20:01:45 +02:00
2026-09-12 09:41:39 +02:00
foreach ( var change in corrected )
2026-09-11 20:01:45 +02:00
{
var entry = new CorpusEntry ( change . Provider , change . ModelId , CorpusOrigin . NAMED_BY_NO_RULE );
var rebuilt = CapabilitySnapshot . Describe ( RebuiltRules . Ask ( entry ));
Assert . That ( rebuilt , Is . EqualTo ( CapabilitySnapshot . Describe ( change . AnswerWanted )), $"{change.Provider} \" { change . ModelId } \ ": {change.Reason}" );
}
});
}
[Test]
2026-09-12 09:41:39 +02:00
public void EveryModelOfTheCorpusIsEitherAnsweredByARuleOrLeftToTheDefaultOnPurpose ()
2026-09-11 20:01:45 +02:00
{
//
2026-09-12 09:41:39 +02:00
// Comparing answers alone cannot catch a model falling through. It gets an empty profile,
// the global default answers for it, and nothing about that looks wrong from the outside --
// a family nobody got round to and a family nobody wanted are both simply missing. This is
// the test which makes the difference visible, by asking for the reason.
2026-09-11 21:35:47 +02:00
//
2026-09-12 09:41:39 +02:00
var fallenThrough = ModelCorpus . ENTRIES
. Where ( entry => ! IsAnswered ( entry ))
. Where ( entry => ! IsLeftToTheDefault ( entry ))
. Select ( entry => $"{entry.Provider} \" { entry . ModelId } \ "" );
2026-09-11 20:01:45 +02:00
2026-09-12 09:41:39 +02:00
Assert . That ( fallenThrough , Is . Empty , "No rule answers for these, and nothing says that is on purpose. Write a family for them, or put them into LeftToTheDefault with the reason." );
}
2026-09-11 20:01:45 +02:00
2026-09-12 09:41:39 +02:00
[Test]
public void NothingLeftToTheDefaultIsAnsweredByARuleAfterAll ()
{
//
// The other direction, so the list cannot rot: once a family is written, the models it
// answers for have no business standing among the ones nobody wrote a rule for.
//
var answeredAfterAll = LeftToTheDefault . ENTRIES
. Where ( left => IsAnswered ( left . Provider , left . ModelId ))
. Select ( left => $"{left.Provider} \" { left . ModelId } \ "" );
Assert . That ( answeredAfterAll , Is . Empty , "A rule answers for these now, so they can be taken off the list of models left to the default." );
2026-09-11 21:35:47 +02:00
}
[Test]
public void NoModelOfTheCorpusIsClaimedByTwoRulesWithTheSameRight ()
{
//
2026-09-12 09:41:39 +02:00
// Two rules of the same specificity which can both match one name are a mistake, not a coin
// toss. Reading the rules alone cannot find it -- the two patterns are written differently
// and only meet on a real name, which is what the corpus is full of.
2026-09-11 21:35:47 +02:00
//
Assert . Multiple (() =>
{
foreach ( var entry in ModelCorpus . ENTRIES )
2026-09-11 20:01:45 +02:00
{
var resolution = ModelRegistry . Shared . Explain ( entry . Provider , entry . ModelId );
Assert . That ( resolution . IsAmbiguous , Is . False , $"{entry.Provider} \" { entry . ModelId } \ " is claimed by {resolution.Selector} and, just as strongly, by {string.Join(" , ", resolution.TiedSelectors)}." );
}
});
}
/// <summary>
2026-09-12 09:41:39 +02:00
/// Whether any rule knows this model.
2026-09-11 20:01:45 +02:00
/// </summary>
2026-09-12 09:41:39 +02:00
/// <param name="entry">The corpus entry to ask about.</param>
/// <returns>True, when a rule answers for it.</returns>
private static bool IsAnswered ( CorpusEntry entry ) => IsAnswered ( entry . Provider , entry . ModelId );
2026-09-11 21:35:47 +02:00
/// <summary>
2026-09-12 09:41:39 +02:00
/// Whether any rule knows this model.
2026-09-11 21:35:47 +02:00
/// </summary>
/// <param name="provider">Who serves the model.</param>
/// <param name="modelId">The model ID as that provider reports it.</param>
2026-09-12 09:41:39 +02:00
/// <returns>True, when a rule answers for it.</returns>
private static bool IsAnswered ( LLMProviders provider , string modelId ) => ModelRegistry . Shared . Explain ( provider , modelId ). IsKnown ;
2026-09-11 21:35:47 +02:00
/// <summary>
2026-09-12 09:41:39 +02:00
/// Whether this model reaches the global default because somebody decided it may.
2026-09-11 21:35:47 +02:00
/// </summary>
2026-09-12 09:41:39 +02:00
/// <param name="entry">The corpus entry to look up.</param>
/// <returns>True, when it stands in the list of models left to the default.</returns>
private static bool IsLeftToTheDefault ( CorpusEntry entry ) => LeftToTheDefault . ENTRIES . Any ( left => left . Provider == entry . Provider && string . Equals ( left . ModelId , entry . ModelId , StringComparison . Ordinal ));
2026-09-11 20:01:45 +02:00
/// <summary>
/// Whether the audit found the current answer for this entry wrong.
/// </summary>
/// <param name="entry">The entry to look up.</param>
/// <returns>True, when the rebuild is meant to answer differently.</returns>
private static bool IsKnownToBeWrong ( CorpusEntry entry ) => ExpectedChanges . ENTRIES . Any ( change => change . Provider == entry . Provider && change . ModelId == entry . ModelId );
}