using System.Text; using Morph.AiKit.Media; using Xunit; namespace Morph.AiKit.Tests; /// /// Canarios de : a chave que dedupe a CHAMADA PAGA. /// /// /// Duas propriedades, e as duas custam dinheiro quando falham. Estabilidade: o mesmo /// pedido tem de dar a mesma chave, senao todo retry paga de novo. Sensibilidade: /// qualquer campo participante que mude tem de mudar a chave, senao um pedido reusa o /// artefato pago de OUTRO — pior que pagar duas vezes. /// public sealed class MediaIdempotencyKeyTests { private static MediaRequest Request( string prompt = "um sofa verde", MediaOptions? options = null, params string[] references) => new("catalogo", prompt, MediaPurpose.CatalogPhoto) { Options = options ?? MediaOptions.None, References = [.. references.Select(r => new MediaReference(r, "image/png", Encoding.UTF8.GetBytes(r)))], }; private static string KeyOf(MediaRequest request) => MediaIdempotencyKey.From(request, "openai", "gpt-image-2").Value; // --------------------------------------------------------------------------------- // Estabilidade // --------------------------------------------------------------------------------- /// Duas construcoes do mesmo pedido dao a mesma chave. [Fact] public void TheSameRequestAlwaysGivesTheSameKey() => Assert.Equal(KeyOf(Request()), KeyOf(Request())); /// /// Espaco em branco a mais nao muda o pedido, entao nao muda a chave. E o que impede um /// prompt re-serializado com quebra de linha diferente de virar outra fatura. /// [Fact] public void WhitespaceIsNormalisedAway() => Assert.Equal(KeyOf(Request("um sofa verde")), KeyOf(Request(" um sofa\n verde "))); /// /// A caixa NAO e normalizada, e e deliberado: num prompt ela muda o resultado (nome de /// marca, texto literal que vai aparecer na imagem). /// [Fact] public void CaseIsPreservedBecauseItChangesThePicture() => Assert.NotEqual(KeyOf(Request("um sofa VERDE")), KeyOf(Request("um sofa verde"))); /// A chave e um sha256 em hex minusculo: 64 digitos. [Fact] public void TheKeyIsASha256InLowercaseHex() { var key = KeyOf(Request()); Assert.Equal(64, key.Length); Assert.Matches("^[0-9a-f]{64}$", key); } // --------------------------------------------------------------------------------- // Sensibilidade — uma rodada POR CAMPO participante // --------------------------------------------------------------------------------- /// Prompt diferente, chave diferente. [Fact] public void ADifferentPromptChangesTheKey() => Assert.NotEqual(KeyOf(Request("um sofa azul")), KeyOf(Request())); /// Modelo diferente, chave diferente — o alias pode ser repontado. [Fact] public void ADifferentModelChangesTheKey() => Assert.NotEqual( MediaIdempotencyKey.From(Request(), "openai", "gpt-image-2").Value, MediaIdempotencyKey.From(Request(), "openai", "outro-modelo").Value); /// Provider diferente, chave diferente. [Fact] public void ADifferentProviderChangesTheKey() => Assert.NotEqual( MediaIdempotencyKey.From(Request(), "openai", "m").Value, MediaIdempotencyKey.From(Request(), "google", "m").Value); /// /// Trocar a foto de referencia E outro pedido, mesmo com o prompt identico. Por /// isso o sha256 de CADA referencia participa, e nao a contagem delas. /// [Fact] public void ADifferentReferenceChangesTheKey() => Assert.NotEqual( KeyOf(Request(references: ["foto-a"])), KeyOf(Request(references: ["foto-b"]))); /// Mesma quantidade de referencias, conteudo trocado de ordem: outro pedido. [Fact] public void TheOrderOfReferencesIsPartOfTheRequest() => Assert.NotEqual( KeyOf(Request(references: ["a", "b"])), KeyOf(Request(references: ["b", "a"]))); /// Sem referencia e com referencia sao pedidos diferentes. [Fact] public void AddingAReferenceChangesTheKey() => Assert.NotEqual(KeyOf(Request()), KeyOf(Request(references: ["foto-a"]))); /// Cada opcao participante, uma rodada. [Theory] [MemberData(nameof(OptionVariations))] public void EveryParticipatingOptionChangesTheKey(MediaOptions variation) => Assert.NotEqual(KeyOf(Request()), KeyOf(Request(options: variation))); /// As variacoes de opcao exercitadas, uma por campo canonico. public static TheoryData OptionVariations() => [ new MediaOptions { Quality = MediaQuality.High }, new MediaOptions { Count = 4 }, new MediaOptions { Width = 1024, Height = 1024 }, new MediaOptions { MediaType = "image/webp" }, new MediaOptions { DurationSeconds = 8 }, new MediaOptions { AspectRatio = "9:16" }, ]; /// /// O separador e um caractere de CONTROLE, e nao |. Um prompt que contivesse o /// separador visivel emendaria campos e faria dois pedidos diferentes colidirem — que /// e o defeito que faz um pedido reusar o artefato pago do outro. /// [Fact] public void APromptFullOfPipesDoesNotCollideWithAnotherRequest() => Assert.NotEqual( KeyOf(Request("gpt-image-2|Image|um sofa")), KeyOf(Request("um sofa"))); /// Normalizacao de prompt e pura e conferivel sozinha. [Theory] [InlineData(" a b ", "a b")] [InlineData("a\n\nb", "a b")] [InlineData("", "")] [InlineData(" ", "")] public void NormalizePromptCollapsesWhitespace(string input, string expected) => Assert.Equal(expected, MediaIdempotencyKey.NormalizePrompt(input)); }