using System.Reflection; using Morph.AiKit.Media; using Xunit; namespace Morph.AiKit.Tests; /// /// Canários da MARCAÇÃO — a quarta afirmação da rubrica `media-claims`, e a última que este /// kit ainda prendia só por prosa. /// /// /// /// A assimetria que estes testes fecham, medida: a regra do turno de conversa estava /// presa por TIPO (`internal` + id de job em vez de bytes) e provada por mutação; custo e /// revisão humana, por ESTADO. A marcação — que é a obrigação legal, o art. 50 do AI Act que /// motivou a issue — estava escrita em `MediaJobState.cs` ("publicado, com a marcacao /// aplicada") e em `MediaRequest.cs` ("marcacao visivel obrigatoria"), e um `grep` por label, /// watermark ou disclosure no kit inteiro voltava vazio. Zero mecanismo. /// /// /// Uma rubrica que cobra de terceiros uma prova que o próprio exemplar não dá é o defeito que /// esta epic catalogou. Estes testes são o inverso literal dele. /// /// /// Fronteira honesta: nada aqui prova que o rótulo chegou aos olhos de alguém — a /// renderização é do aplicativo consumidor, e nenhum tipo em C# alcança um pixel. O que se /// prova é que a publicação não acontece sem a decisão registrada, com texto e /// superfície nomeados. /// /// public sealed class MediaDisclosureTests { private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero); private static MediaDisclosure Label => MediaDisclosure.Create("Imagem gerada por IA", "legenda", T0); /// Um job aprovado, na finalidade pedida. private static MediaJob Approved(MediaPurpose purpose) { var job = MediaJob.Create( new MediaRequest("catalogo", "um sofa verde", purpose), new MediaIdempotencyKey("chave"), T0).StartGenerating().Value; job = job.MarkGenerated( new MediaHash("abc"), "ab/c/abc.png", new MediaProvenance("openai", "gpt-image-2", "p", null, [], "-", null, 0m, T0, true)).Value; return job.Approve(MediaReview.ApprovedBy("lucas", T0)).Value; } // --------------------------------------------------------------------------------- // A política, por finalidade — dado, não comentário // --------------------------------------------------------------------------------- /// Catálogo e marketing exigem marcação; rascunho não chega a publicar. [Theory] [InlineData(MediaPurpose.CatalogPhoto, true)] [InlineData(MediaPurpose.Marketing, true)] [InlineData(MediaPurpose.Draft, false)] public void RequiresDisclosureIsDeclaredPerPurpose(MediaPurpose purpose, bool expected) => Assert.Equal(expected, purpose.RequiresDisclosure()); /// Rascunho é a única finalidade não publicável. [Theory] [InlineData(MediaPurpose.CatalogPhoto, true)] [InlineData(MediaPurpose.Marketing, true)] [InlineData(MediaPurpose.Draft, false)] public void OnlyDraftIsUnpublishable(MediaPurpose purpose, bool expected) => Assert.Equal(expected, purpose.IsPublishable()); // --------------------------------------------------------------------------------- // A porta: publicar sem marcação NÃO é um caminho // --------------------------------------------------------------------------------- /// /// O caso central. Um job aprovado, de catálogo, sem marcação: não publica, e a /// razão nomeia o que falta. /// [Theory] [InlineData(MediaPurpose.CatalogPhoto)] [InlineData(MediaPurpose.Marketing)] public void PublishFailsWithoutADisclosureWhenThePurposeDemandsOne(MediaPurpose purpose) { var result = Approved(purpose).Publish(null, T0); Assert.False( result.IsSuccess, "publicar sem marcacao e o caminho que a obrigacao legal existe para fechar; " + "se ele existe, a afirmacao da rubrica media-claims esta presa so por prosa"); Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code); Assert.Contains("marcacao", result.Error.Message, StringComparison.OrdinalIgnoreCase); } /// Com a marcação, publica — e ela fica GRAVADA no job, não descartada. [Fact] public void PublishSucceedsWithADisclosureAndRecordsIt() { var published = Approved(MediaPurpose.CatalogPhoto).Publish(Label, T0); Assert.True(published.IsSuccess); Assert.Equal(MediaJobState.Published, published.Value.State); Assert.Equal("Imagem gerada por IA", published.Value.Disclosure!.Text); Assert.Equal("legenda", published.Value.Disclosure.Surface); Assert.Equal(T0, published.Value.Disclosure.AppliedAt); } /// /// Rascunho não publica, nem com marcação e nem estando aprovado. Era uma frase /// num comentário XML; agora é uma recusa. /// [Fact] public void ADraftNeverPublishesEvenApprovedAndLabelled() { var result = Approved(MediaPurpose.Draft).Publish(Label, T0); Assert.False(result.IsSuccess); Assert.Contains("nao publica", result.Error!.Message, StringComparison.OrdinalIgnoreCase); } /// /// A ordem das portas importa: o estado vem antes da marcação. Um job NÃO aprovado, sem /// marcação, tem de reclamar da revisão humana — mandar consertar a marcação de algo que /// nem foi revisado manda consertar a coisa errada. /// [Fact] public void TheStateGateIsReportedBeforeTheDisclosureGate() { var generated = MediaJob.Create( new MediaRequest("catalogo", "p", MediaPurpose.CatalogPhoto), new MediaIdempotencyKey("k"), T0).StartGenerating().Value .MarkGenerated( new MediaHash("abc"), "ab/c/abc.png", new MediaProvenance("openai", "m", "p", null, [], "-", null, 0m, T0, true)).Value; var result = generated.Publish(null, T0); Assert.False(result.IsSuccess); Assert.Contains("Approved", result.Error!.Message, StringComparison.Ordinal); Assert.DoesNotContain("marcacao", result.Error.Message, StringComparison.OrdinalIgnoreCase); } // --------------------------------------------------------------------------------- // A marcação vazia é o campo preenchido "para passar" // --------------------------------------------------------------------------------- /// /// A construção não tem outra porta. Este é o teste que o nome do anterior prometia /// e não entregava. /// /// /// /// A primeira versão de era um record POSICIONAL, e record /// posicional gera construtor público. A validação vivia só em /// , então um consumidor externo fazia /// new MediaDisclosure("", "", at) e publicava com rótulo vazio — medido: /// IsSuccess=True State=Published. O canário de então exercitava a FÁBRICA e passava /// verde o tempo todo: media a copia, nao o objeto. /// /// /// Por isso a asserção é por REFLEXÃO sobre o tipo. Ela não pode ser escrita como /// new MediaDisclosure(...) — hoje isso nem compila, e é esse o ponto: uma asserção /// que só existe enquanto o defeito existe não é canário. /// /// [Fact] public void TheTypeExposesNoPublicConstructor() { var publicCtors = typeof(MediaDisclosure) .GetConstructors(BindingFlags.Public | BindingFlags.Instance); Assert.True( publicCtors.Length == 0, "MediaDisclosure nao pode ter construtor publico: ele contorna Create e deixa " + "passar marcacao em branco — o campo preenchido 'para passar' que este tipo existe " + "para impedir. Record POSICIONAL gera um; use record com construtor privado, como " + "MediaReview. Encontrados: " + string.Join(" | ", publicCtors.Select(c => c.ToString()))); } /// Texto ou superfície em branco é recusado pela única porta que existe. [Theory] [InlineData("", "legenda")] [InlineData(" ", "legenda")] [InlineData("Imagem gerada por IA", "")] [InlineData("Imagem gerada por IA", " ")] public void ABlankDisclosureIsRefusedByTheOnlyFactory(string text, string surface) => Assert.Throws(() => MediaDisclosure.Create(text, surface, T0)); /// /// A superfície é obrigatória junto do texto: "marcado" sem dizer ONDE não responde se /// alguém consegue ver. /// [Fact] public void TheDisclosureCarriesBothTheTextAndWhereItAppears() { var disclosure = MediaDisclosure.Create("Gerado por IA", "sobreposicao", T0); Assert.Equal("Gerado por IA", disclosure.Text); Assert.Equal("sobreposicao", disclosure.Surface); } /// Uma publicação recusada não muda o job — o registro continua imutável. [Fact] public void ARefusedPublishLeavesTheJobUntouched() { var job = Approved(MediaPurpose.Marketing); _ = job.Publish(null, T0); Assert.Equal(MediaJobState.Approved, job.State); Assert.Null(job.Disclosure); Assert.Null(job.PublishedAt); } }