@polymorphism-tech/morph-spec 8.38.0 → 8.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +20 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +59 -25
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -5,15 +5,6 @@ using Xunit;
|
|
|
5
5
|
|
|
6
6
|
namespace Morph.AiKit.Tests;
|
|
7
7
|
|
|
8
|
-
/// <summary>
|
|
9
|
-
/// Canarios do enqueue e do worker. <b>Nenhum deles toca API paga</b> — o unico ponto que
|
|
10
|
-
/// gastaria dinheiro e o <see cref="FakeMediaProvider"/>, e a suite conta as chamadas dele.
|
|
11
|
-
/// </summary>
|
|
12
|
-
/// <remarks>
|
|
13
|
-
/// As afirmacoes aqui sao as tres que o dinheiro cobra: um retry nao paga de novo, um
|
|
14
|
-
/// estouro de teto recusa <b>antes</b> da chamada, e um bloqueio de moderacao vai para
|
|
15
|
-
/// <c>RejectedByPolicy</c> em vez de virar retry.
|
|
16
|
-
/// </remarks>
|
|
17
8
|
public sealed class MediaGeneratorTests
|
|
18
9
|
{
|
|
19
10
|
private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
|
|
@@ -42,11 +33,6 @@ public sealed class MediaGeneratorTests
|
|
|
42
33
|
jobs, store, provider);
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
// ---------------------------------------------------------------------------------
|
|
46
|
-
// Enqueue: id de job, nunca bytes
|
|
47
|
-
// ---------------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/// <summary>Um pedido valido vira job em <c>Pending</c>, e o retorno e um id.</summary>
|
|
50
36
|
[Fact]
|
|
51
37
|
public async Task EnqueueReturnsAJobIdAndTheJobStartsPending()
|
|
52
38
|
{
|
|
@@ -58,11 +44,9 @@ public sealed class MediaGeneratorTests
|
|
|
58
44
|
var job = await generator.GetAsync(enqueued.Value, Ct);
|
|
59
45
|
Assert.Equal(MediaJobState.Pending, job.Value.State);
|
|
60
46
|
|
|
61
|
-
// Nada foi gasto: o enqueue NAO chama provider.
|
|
62
47
|
Assert.Equal(0, provider.Calls);
|
|
63
48
|
}
|
|
64
49
|
|
|
65
|
-
/// <summary>Pedido sem prompt e recusa nomeada, nao excecao.</summary>
|
|
66
50
|
[Fact]
|
|
67
51
|
public async Task AnInvalidRequestIsRefusedByName()
|
|
68
52
|
{
|
|
@@ -75,7 +59,6 @@ public sealed class MediaGeneratorTests
|
|
|
75
59
|
Assert.Equal(MediaErrorCode.InvalidRequest, result.Error!.Code);
|
|
76
60
|
}
|
|
77
61
|
|
|
78
|
-
/// <summary>Alias desconhecido: recusa, e nenhum job criado.</summary>
|
|
79
62
|
[Fact]
|
|
80
63
|
public async Task AnUnknownAliasIsRefused()
|
|
81
64
|
{
|
|
@@ -88,14 +71,6 @@ public sealed class MediaGeneratorTests
|
|
|
88
71
|
Assert.Equal(0, jobs.Saves);
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
// ---------------------------------------------------------------------------------
|
|
92
|
-
// Idempotencia: o retry nao paga de novo
|
|
93
|
-
// ---------------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
/// <summary>
|
|
96
|
-
/// O MESMO pedido, enfileirado duas vezes, devolve o MESMO job. E o que faz um retry de
|
|
97
|
-
/// fila, de worker reiniciado ou de webhook duplicado custar zero.
|
|
98
|
-
/// </summary>
|
|
99
74
|
[Fact]
|
|
100
75
|
public async Task TheSameRequestEnqueuedTwiceReusesTheSameJob()
|
|
101
76
|
{
|
|
@@ -108,7 +83,6 @@ public sealed class MediaGeneratorTests
|
|
|
108
83
|
Assert.Equal(1, jobs.Saves);
|
|
109
84
|
}
|
|
110
85
|
|
|
111
|
-
/// <summary>Pedido diferente, job diferente — a dedupe nao pode engolir trabalho novo.</summary>
|
|
112
86
|
[Fact]
|
|
113
87
|
public async Task ADifferentRequestGetsItsOwnJob()
|
|
114
88
|
{
|
|
@@ -121,14 +95,6 @@ public sealed class MediaGeneratorTests
|
|
|
121
95
|
Assert.NotEqual(first.Value, second.Value);
|
|
122
96
|
}
|
|
123
97
|
|
|
124
|
-
// ---------------------------------------------------------------------------------
|
|
125
|
-
// Teto de custo: recusa ANTES da chamada
|
|
126
|
-
// ---------------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
/// <summary>
|
|
129
|
-
/// O teto e confrontado com o custo estimado, e a recusa acontece no enqueue — antes de
|
|
130
|
-
/// existir qualquer chamada. Depois dela, o dinheiro ja saiu.
|
|
131
|
-
/// </summary>
|
|
132
98
|
[Fact]
|
|
133
99
|
public async Task ABudgetOverrunIsRefusedAtEnqueueBeforeAnyCall()
|
|
134
100
|
{
|
|
@@ -145,7 +111,6 @@ public sealed class MediaGeneratorTests
|
|
|
145
111
|
Assert.Equal([0.211m], budget.Seen);
|
|
146
112
|
}
|
|
147
113
|
|
|
148
|
-
/// <summary>O custo estimado que chega ao teto e o da tarifa, na quantidade pedida.</summary>
|
|
149
114
|
[Fact]
|
|
150
115
|
public async Task TheEstimateSentToTheBudgetIsTariffTimesCount()
|
|
151
116
|
{
|
|
@@ -157,11 +122,6 @@ public sealed class MediaGeneratorTests
|
|
|
157
122
|
Assert.Equal([0.024m], budget.Seen);
|
|
158
123
|
}
|
|
159
124
|
|
|
160
|
-
// ---------------------------------------------------------------------------------
|
|
161
|
-
// Worker: estados, storage e a distincao que vale dinheiro
|
|
162
|
-
// ---------------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
/// <summary>Caminho feliz: os bytes vao para o storage ANTES de o job virar Generated.</summary>
|
|
165
125
|
[Fact]
|
|
166
126
|
public async Task TheWorkerStoresTheBytesAndThenMarksGenerated()
|
|
167
127
|
{
|
|
@@ -178,7 +138,6 @@ public sealed class MediaGeneratorTests
|
|
|
178
138
|
Assert.EndsWith(".png", ran.Value.RelativePath, StringComparison.Ordinal);
|
|
179
139
|
}
|
|
180
140
|
|
|
181
|
-
/// <summary>A proveniencia e gravada com o modelo, o prompt reescrito e o custo estimado.</summary>
|
|
182
141
|
[Fact]
|
|
183
142
|
public async Task TheWorkerRecordsProvenanceWithTheEstimatedCost()
|
|
184
143
|
{
|
|
@@ -197,10 +156,6 @@ public sealed class MediaGeneratorTests
|
|
|
197
156
|
Assert.True(provenance.C2paPreserved);
|
|
198
157
|
}
|
|
199
158
|
|
|
200
|
-
/// <summary>
|
|
201
|
-
/// Bloqueio de moderacao vira <c>RejectedByPolicy</c>, terminal. Um erro tecnico vira
|
|
202
|
-
/// <c>Failed</c>, retryable. Colapsar os dois e como um laco de retry vira fatura.
|
|
203
|
-
/// </summary>
|
|
204
159
|
[Theory]
|
|
205
160
|
[InlineData(MediaErrorCode.PolicyBlocked, MediaJobState.RejectedByPolicy, false)]
|
|
206
161
|
[InlineData(MediaErrorCode.ProviderFailure, MediaJobState.Failed, true)]
|
|
@@ -218,7 +173,6 @@ public sealed class MediaGeneratorTests
|
|
|
218
173
|
Assert.Equal(0, store.Blobs.Writes);
|
|
219
174
|
}
|
|
220
175
|
|
|
221
|
-
/// <summary>Job inexistente e recusa nomeada, nunca uma excecao de referencia nula.</summary>
|
|
222
176
|
[Fact]
|
|
223
177
|
public async Task RunningAnUnknownJobIsRefusedByName()
|
|
224
178
|
{
|
|
@@ -230,10 +184,6 @@ public sealed class MediaGeneratorTests
|
|
|
230
184
|
Assert.Equal(MediaErrorCode.NotFound, ran.Error!.Code);
|
|
231
185
|
}
|
|
232
186
|
|
|
233
|
-
/// <summary>
|
|
234
|
-
/// Um alias de VIDEO pedido como imagem nao vira job: as unidades de cobranca sao
|
|
235
|
-
/// diferentes, e um pedido com a tarifa errada estima o custo errado em silencio.
|
|
236
|
-
/// </summary>
|
|
237
187
|
[Fact]
|
|
238
188
|
public async Task AVideoAliasCannotServeAnImageRequest()
|
|
239
189
|
{
|
|
@@ -246,9 +196,6 @@ public sealed class MediaGeneratorTests
|
|
|
246
196
|
Assert.Equal(MediaErrorCode.InvalidRequest, result.Error!.Code);
|
|
247
197
|
}
|
|
248
198
|
|
|
249
|
-
/// <summary>
|
|
250
|
-
/// Video sem duracao nao tem custo estimavel — e arbitrar segundos e arbitrar dinheiro.
|
|
251
|
-
/// </summary>
|
|
252
199
|
[Fact]
|
|
253
200
|
public async Task AVideoRequestWithoutDurationCannotBePriced()
|
|
254
201
|
{
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs
CHANGED
|
@@ -4,15 +4,6 @@ using Xunit;
|
|
|
4
4
|
|
|
5
5
|
namespace Morph.AiKit.Tests;
|
|
6
6
|
|
|
7
|
-
/// <summary>
|
|
8
|
-
/// Canarios de <see cref="MediaIdempotencyKey"/>: a chave que dedupe a CHAMADA PAGA.
|
|
9
|
-
/// </summary>
|
|
10
|
-
/// <remarks>
|
|
11
|
-
/// Duas propriedades, e as duas custam dinheiro quando falham. <b>Estabilidade</b>: o mesmo
|
|
12
|
-
/// pedido tem de dar a mesma chave, senao todo retry paga de novo. <b>Sensibilidade</b>:
|
|
13
|
-
/// qualquer campo participante que mude tem de mudar a chave, senao um pedido reusa o
|
|
14
|
-
/// artefato pago de OUTRO — pior que pagar duas vezes.
|
|
15
|
-
/// </remarks>
|
|
16
7
|
public sealed class MediaIdempotencyKeyTests
|
|
17
8
|
{
|
|
18
9
|
private static MediaRequest Request(
|
|
@@ -29,32 +20,18 @@ public sealed class MediaIdempotencyKeyTests
|
|
|
29
20
|
private static string KeyOf(MediaRequest request)
|
|
30
21
|
=> MediaIdempotencyKey.From(request, "openai", "gpt-image-2").Value;
|
|
31
22
|
|
|
32
|
-
// ---------------------------------------------------------------------------------
|
|
33
|
-
// Estabilidade
|
|
34
|
-
// ---------------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/// <summary>Duas construcoes do mesmo pedido dao a mesma chave.</summary>
|
|
37
23
|
[Fact]
|
|
38
24
|
public void TheSameRequestAlwaysGivesTheSameKey()
|
|
39
25
|
=> Assert.Equal(KeyOf(Request()), KeyOf(Request()));
|
|
40
26
|
|
|
41
|
-
/// <summary>
|
|
42
|
-
/// Espaco em branco a mais nao muda o pedido, entao nao muda a chave. E o que impede um
|
|
43
|
-
/// prompt re-serializado com quebra de linha diferente de virar outra fatura.
|
|
44
|
-
/// </summary>
|
|
45
27
|
[Fact]
|
|
46
28
|
public void WhitespaceIsNormalisedAway()
|
|
47
29
|
=> Assert.Equal(KeyOf(Request("um sofa verde")), KeyOf(Request(" um sofa\n verde ")));
|
|
48
30
|
|
|
49
|
-
/// <summary>
|
|
50
|
-
/// A caixa NAO e normalizada, e e deliberado: num prompt ela muda o resultado (nome de
|
|
51
|
-
/// marca, texto literal que vai aparecer na imagem).
|
|
52
|
-
/// </summary>
|
|
53
31
|
[Fact]
|
|
54
32
|
public void CaseIsPreservedBecauseItChangesThePicture()
|
|
55
33
|
=> Assert.NotEqual(KeyOf(Request("um sofa VERDE")), KeyOf(Request("um sofa verde")));
|
|
56
34
|
|
|
57
|
-
/// <summary>A chave e um sha256 em hex minusculo: 64 digitos.</summary>
|
|
58
35
|
[Fact]
|
|
59
36
|
public void TheKeyIsASha256InLowercaseHex()
|
|
60
37
|
{
|
|
@@ -64,58 +41,43 @@ public sealed class MediaIdempotencyKeyTests
|
|
|
64
41
|
Assert.Matches("^[0-9a-f]{64}$", key);
|
|
65
42
|
}
|
|
66
43
|
|
|
67
|
-
// ---------------------------------------------------------------------------------
|
|
68
|
-
// Sensibilidade — uma rodada POR CAMPO participante
|
|
69
|
-
// ---------------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
/// <summary>Prompt diferente, chave diferente.</summary>
|
|
72
44
|
[Fact]
|
|
73
45
|
public void ADifferentPromptChangesTheKey()
|
|
74
46
|
=> Assert.NotEqual(KeyOf(Request("um sofa azul")), KeyOf(Request()));
|
|
75
47
|
|
|
76
|
-
/// <summary>Modelo diferente, chave diferente — o alias pode ser repontado.</summary>
|
|
77
48
|
[Fact]
|
|
78
49
|
public void ADifferentModelChangesTheKey()
|
|
79
50
|
=> Assert.NotEqual(
|
|
80
51
|
MediaIdempotencyKey.From(Request(), "openai", "gpt-image-2").Value,
|
|
81
52
|
MediaIdempotencyKey.From(Request(), "openai", "outro-modelo").Value);
|
|
82
53
|
|
|
83
|
-
/// <summary>Provider diferente, chave diferente.</summary>
|
|
84
54
|
[Fact]
|
|
85
55
|
public void ADifferentProviderChangesTheKey()
|
|
86
56
|
=> Assert.NotEqual(
|
|
87
57
|
MediaIdempotencyKey.From(Request(), "openai", "m").Value,
|
|
88
58
|
MediaIdempotencyKey.From(Request(), "google", "m").Value);
|
|
89
59
|
|
|
90
|
-
/// <summary>
|
|
91
|
-
/// <b>Trocar a foto de referencia E outro pedido</b>, mesmo com o prompt identico. Por
|
|
92
|
-
/// isso o sha256 de CADA referencia participa, e nao a contagem delas.
|
|
93
|
-
/// </summary>
|
|
94
60
|
[Fact]
|
|
95
61
|
public void ADifferentReferenceChangesTheKey()
|
|
96
62
|
=> Assert.NotEqual(
|
|
97
63
|
KeyOf(Request(references: ["foto-a"])),
|
|
98
64
|
KeyOf(Request(references: ["foto-b"])));
|
|
99
65
|
|
|
100
|
-
/// <summary>Mesma quantidade de referencias, conteudo trocado de ordem: outro pedido.</summary>
|
|
101
66
|
[Fact]
|
|
102
67
|
public void TheOrderOfReferencesIsPartOfTheRequest()
|
|
103
68
|
=> Assert.NotEqual(
|
|
104
69
|
KeyOf(Request(references: ["a", "b"])),
|
|
105
70
|
KeyOf(Request(references: ["b", "a"])));
|
|
106
71
|
|
|
107
|
-
/// <summary>Sem referencia e com referencia sao pedidos diferentes.</summary>
|
|
108
72
|
[Fact]
|
|
109
73
|
public void AddingAReferenceChangesTheKey()
|
|
110
74
|
=> Assert.NotEqual(KeyOf(Request()), KeyOf(Request(references: ["foto-a"])));
|
|
111
75
|
|
|
112
|
-
/// <summary>Cada opcao participante, uma rodada.</summary>
|
|
113
76
|
[Theory]
|
|
114
77
|
[MemberData(nameof(OptionVariations))]
|
|
115
78
|
public void EveryParticipatingOptionChangesTheKey(MediaOptions variation)
|
|
116
79
|
=> Assert.NotEqual(KeyOf(Request()), KeyOf(Request(options: variation)));
|
|
117
80
|
|
|
118
|
-
/// <summary>As variacoes de opcao exercitadas, uma por campo canonico.</summary>
|
|
119
81
|
public static TheoryData<MediaOptions> OptionVariations() =>
|
|
120
82
|
[
|
|
121
83
|
new MediaOptions { Quality = MediaQuality.High },
|
|
@@ -126,18 +88,12 @@ public sealed class MediaIdempotencyKeyTests
|
|
|
126
88
|
new MediaOptions { AspectRatio = "9:16" },
|
|
127
89
|
];
|
|
128
90
|
|
|
129
|
-
/// <summary>
|
|
130
|
-
/// O separador e um caractere de CONTROLE, e nao <c>|</c>. Um prompt que contivesse o
|
|
131
|
-
/// separador visivel emendaria campos e faria dois pedidos diferentes colidirem — que
|
|
132
|
-
/// e o defeito que faz um pedido reusar o artefato pago do outro.
|
|
133
|
-
/// </summary>
|
|
134
91
|
[Fact]
|
|
135
92
|
public void APromptFullOfPipesDoesNotCollideWithAnotherRequest()
|
|
136
93
|
=> Assert.NotEqual(
|
|
137
94
|
KeyOf(Request("gpt-image-2|Image|um sofa")),
|
|
138
95
|
KeyOf(Request("um sofa")));
|
|
139
96
|
|
|
140
|
-
/// <summary>Normalizacao de prompt e pura e conferivel sozinha.</summary>
|
|
141
97
|
[Theory]
|
|
142
98
|
[InlineData(" a b ", "a b")]
|
|
143
99
|
[InlineData("a\n\nb", "a b")]
|
|
@@ -3,23 +3,10 @@ using Xunit;
|
|
|
3
3
|
|
|
4
4
|
namespace Morph.AiKit.Tests;
|
|
5
5
|
|
|
6
|
-
/// <summary>
|
|
7
|
-
/// Canarios da maquina de estados de <see cref="MediaJob"/>.
|
|
8
|
-
/// </summary>
|
|
9
|
-
/// <remarks>
|
|
10
|
-
/// Tres afirmacoes carregam o standard <c>ai-agents/media-pipeline.md</c> inteiro, e cada
|
|
11
|
-
/// uma e o inverso literal de um defeito caro:
|
|
12
|
-
/// <list type="number">
|
|
13
|
-
/// <item><c>Publish</c> falha fora de <c>Approved</c> — publicar sem revisao humana.</item>
|
|
14
|
-
/// <item><c>Approved</c> so existe por <c>Approve(revisor)</c> — "aprovado" sem quem.</item>
|
|
15
|
-
/// <item><c>RejectedByPolicy</c> nao e retryable — retry que vira fatura.</item>
|
|
16
|
-
/// </list>
|
|
17
|
-
/// </remarks>
|
|
18
6
|
public sealed class MediaJobStateTests
|
|
19
7
|
{
|
|
20
8
|
private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
|
|
21
9
|
|
|
22
|
-
/// <summary>Uma marcacao valida, para os casos que nao estao medindo a marcacao.</summary>
|
|
23
10
|
private static MediaDisclosure Label => MediaDisclosure.Create("Imagem gerada por IA", "legenda", T0);
|
|
24
11
|
|
|
25
12
|
private static MediaJob Pending() => MediaJob.Create(
|
|
@@ -37,14 +24,6 @@ public sealed class MediaJobStateTests
|
|
|
37
24
|
.Value;
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
// ---------------------------------------------------------------------------------
|
|
41
|
-
// 1. Publish so a partir de Approved
|
|
42
|
-
// ---------------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/// <summary>
|
|
45
|
-
/// O atalho que alguem sempre quer: publicar direto de <c>Generated</c>. Ele nao
|
|
46
|
-
/// existe, e a mensagem diz por que.
|
|
47
|
-
/// </summary>
|
|
48
27
|
[Fact]
|
|
49
28
|
public void PublishFailsFromGeneratedBecauseNobodyReviewedIt()
|
|
50
29
|
{
|
|
@@ -55,7 +34,6 @@ public sealed class MediaJobStateTests
|
|
|
55
34
|
Assert.Contains("Approved", result.Error.Message, StringComparison.Ordinal);
|
|
56
35
|
}
|
|
57
36
|
|
|
58
|
-
/// <summary>Publish falha a partir de TODO estado que nao seja Approved — nao so de Generated.</summary>
|
|
59
37
|
[Theory]
|
|
60
38
|
[InlineData(MediaJobState.Pending)]
|
|
61
39
|
[InlineData(MediaJobState.Generating)]
|
|
@@ -71,7 +49,6 @@ public sealed class MediaJobStateTests
|
|
|
71
49
|
Assert.False(job.Publish(Label, T0).IsSuccess);
|
|
72
50
|
}
|
|
73
51
|
|
|
74
|
-
/// <summary>E, com a revisao humana no lugar, publica.</summary>
|
|
75
52
|
[Fact]
|
|
76
53
|
public void PublishSucceedsFromApproved()
|
|
77
54
|
{
|
|
@@ -84,11 +61,6 @@ public sealed class MediaJobStateTests
|
|
|
84
61
|
Assert.Equal(T0, published.Value.PublishedAt);
|
|
85
62
|
}
|
|
86
63
|
|
|
87
|
-
// ---------------------------------------------------------------------------------
|
|
88
|
-
// 2. Approved so por Approve(revisor)
|
|
89
|
-
// ---------------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/// <summary>A aprovacao carrega quem e quando — sem isso e um campo booleano.</summary>
|
|
92
64
|
[Fact]
|
|
93
65
|
public void ApproveStampsTheReviewerAndTheTime()
|
|
94
66
|
{
|
|
@@ -100,15 +72,10 @@ public sealed class MediaJobStateTests
|
|
|
100
72
|
Assert.Equal("cor confere", approved.Review.Note);
|
|
101
73
|
}
|
|
102
74
|
|
|
103
|
-
/// <summary>Revisor em branco nao carimba nada, e a recusa e na construcao da revisao.</summary>
|
|
104
75
|
[Fact]
|
|
105
76
|
public void AnAnonymousReviewIsRefusedAtConstruction()
|
|
106
77
|
=> Assert.Throws<ArgumentException>(() => MediaReview.ApprovedBy(" ", T0));
|
|
107
78
|
|
|
108
|
-
/// <summary>
|
|
109
|
-
/// Aprovar com um veredito de REJEICAO seria registrar o oposto do que o humano
|
|
110
|
-
/// decidiu. O tipo recusa.
|
|
111
|
-
/// </summary>
|
|
112
79
|
[Fact]
|
|
113
80
|
public void ApproveRefusesAReviewThatSaysRejected()
|
|
114
81
|
{
|
|
@@ -118,19 +85,10 @@ public sealed class MediaJobStateTests
|
|
|
118
85
|
Assert.Equal(MediaErrorCode.InvalidTransition, result.Error!.Code);
|
|
119
86
|
}
|
|
120
87
|
|
|
121
|
-
/// <summary>Aprovar antes de existirem bytes nao e transicao.</summary>
|
|
122
88
|
[Fact]
|
|
123
89
|
public void ApproveFailsBeforeTheBytesExist()
|
|
124
90
|
=> Assert.False(Pending().Approve(MediaReview.ApprovedBy("lucas", T0)).IsSuccess);
|
|
125
91
|
|
|
126
|
-
// ---------------------------------------------------------------------------------
|
|
127
|
-
// 3. RejectedByPolicy nao e retryable; Failed e
|
|
128
|
-
// ---------------------------------------------------------------------------------
|
|
129
|
-
|
|
130
|
-
/// <summary>
|
|
131
|
-
/// A distincao que vale dinheiro. Bloqueio de moderacao e resposta definitiva; erro
|
|
132
|
-
/// tecnico e tentativa perdida.
|
|
133
|
-
/// </summary>
|
|
134
92
|
[Fact]
|
|
135
93
|
public void PolicyBlockIsTerminalAndTechnicalFailureIsNot()
|
|
136
94
|
{
|
|
@@ -146,7 +104,6 @@ public sealed class MediaJobStateTests
|
|
|
146
104
|
Assert.True(failed.StartGenerating().IsSuccess);
|
|
147
105
|
}
|
|
148
106
|
|
|
149
|
-
/// <summary>Um job em <c>Generated</c> carrega hash, caminho e proveniencia — os tres juntos.</summary>
|
|
150
107
|
[Fact]
|
|
151
108
|
public void GeneratedCarriesHashPathAndProvenanceTogether()
|
|
152
109
|
{
|
|
@@ -158,7 +115,6 @@ public sealed class MediaJobStateTests
|
|
|
158
115
|
Assert.Equal("gpt-image-2", job.Provenance!.ModelId);
|
|
159
116
|
}
|
|
160
117
|
|
|
161
|
-
/// <summary>Uma transicao recusada NAO muda o job: o registro e imutavel.</summary>
|
|
162
118
|
[Fact]
|
|
163
119
|
public void ARefusedTransitionLeavesTheJobUntouched()
|
|
164
120
|
{
|
|
@@ -3,15 +3,6 @@ using Xunit;
|
|
|
3
3
|
|
|
4
4
|
namespace Morph.AiKit.Tests;
|
|
5
5
|
|
|
6
|
-
/// <summary>
|
|
7
|
-
/// Canarios de <see cref="MediaReview"/>.
|
|
8
|
-
/// </summary>
|
|
9
|
-
/// <remarks>
|
|
10
|
-
/// A afirmacao que este arquivo protege nao e "aprovar funciona" — e que
|
|
11
|
-
/// <see cref="MediaReview"/> <b>delega</b> ao <see cref="MediaJob"/> em vez de reimplementar
|
|
12
|
-
/// a regra. Uma segunda copia de "so Approved publica" e exatamente como as duas divergem, e
|
|
13
|
-
/// e o padrao de falha que esta epic catalogou sete vezes: o teste mede uma COPIA do objeto.
|
|
14
|
-
/// </remarks>
|
|
15
6
|
public sealed class MediaReviewTests
|
|
16
7
|
{
|
|
17
8
|
private static readonly DateTimeOffset T0 = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
|
|
@@ -31,7 +22,6 @@ public sealed class MediaReviewTests
|
|
|
31
22
|
new MediaProvenance("openai", "gpt-image-2", "p", null, [], "-", null, 0m, T0, true)).Value;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
/// <summary>O atalho aprova e carimba.</summary>
|
|
35
25
|
[Fact]
|
|
36
26
|
public void ApproveDelegatesAndStamps()
|
|
37
27
|
{
|
|
@@ -42,7 +32,6 @@ public sealed class MediaReviewTests
|
|
|
42
32
|
Assert.Equal("lucas", result.Value.Review!.Reviewer);
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/// <summary>Rejeitar e terminal e tambem carimba.</summary>
|
|
46
35
|
[Fact]
|
|
47
36
|
public void RejectDelegatesAndStamps()
|
|
48
37
|
{
|
|
@@ -53,12 +42,6 @@ public sealed class MediaReviewTests
|
|
|
53
42
|
Assert.Equal(MediaReviewDecision.Rejected, result.Value.Review!.Decision);
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
/// <summary>
|
|
57
|
-
/// O atalho de publicar NAO tem uma regra propria: ele recusa exatamente onde
|
|
58
|
-
/// <see cref="MediaJob.Publish"/> recusa. Confere-se comparando as DUAS chamadas sobre o
|
|
59
|
-
/// MESMO job — se <c>MediaReview</c> tivesse copiado a regra, aqui e onde a copia
|
|
60
|
-
/// divergiria.
|
|
61
|
-
/// </summary>
|
|
62
45
|
[Fact]
|
|
63
46
|
public void PublishThroughTheReviewFacadeAgreesWithTheJobItself()
|
|
64
47
|
{
|
|
@@ -72,12 +55,10 @@ public sealed class MediaReviewTests
|
|
|
72
55
|
Assert.True(MediaReview.Publish(approved, Label, T0).IsSuccess);
|
|
73
56
|
}
|
|
74
57
|
|
|
75
|
-
/// <summary>Job nulo e falha de composicao, nao Result: quem chama errou o codigo.</summary>
|
|
76
58
|
[Fact]
|
|
77
59
|
public void ANullJobIsAProgrammingError()
|
|
78
60
|
=> Assert.Throws<ArgumentNullException>(() => MediaReview.Approve(null!, "lucas", T0));
|
|
79
61
|
|
|
80
|
-
/// <summary>Um juiz LLM cabe na nota, nunca no campo do revisor de uma foto de catalogo.</summary>
|
|
81
62
|
[Fact]
|
|
82
63
|
public void TheNoteCarriesThePreFilterVerdict()
|
|
83
64
|
{
|
|
@@ -5,22 +5,12 @@ using Xunit;
|
|
|
5
5
|
|
|
6
6
|
namespace Morph.AiKit.Tests;
|
|
7
7
|
|
|
8
|
-
/// <summary>
|
|
9
|
-
/// Canarios do <see cref="MediaStore"/>: endereco por conteudo, e gravacao unica.
|
|
10
|
-
/// </summary>
|
|
11
|
-
/// <remarks>
|
|
12
|
-
/// A armadilha deste arquivo, e a razao de <see cref="InMemoryMediaBlobs.Writes"/> existir:
|
|
13
|
-
/// um teste que so conferisse "o hash e o mesmo" <b>nao distingue dedupe de regravacao</b>,
|
|
14
|
-
/// porque os dois produzem o mesmo hash. A afirmacao "grava uma vez" so e verificavel
|
|
15
|
-
/// contando gravacoes no blob.
|
|
16
|
-
/// </remarks>
|
|
17
8
|
public sealed class MediaStoreTests
|
|
18
9
|
{
|
|
19
10
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
20
11
|
|
|
21
12
|
private static ReadOnlyMemory<byte> Bytes(string s) => Encoding.UTF8.GetBytes(s);
|
|
22
13
|
|
|
23
|
-
/// <summary>Mesmo conteudo, mesmo hash, mesmo caminho — e UMA gravacao.</summary>
|
|
24
14
|
[Fact]
|
|
25
15
|
public async Task TheSameContentIsStoredOnce()
|
|
26
16
|
{
|
|
@@ -37,7 +27,6 @@ public sealed class MediaStoreTests
|
|
|
37
27
|
Assert.Equal(1, blobs.Writes);
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
/// <summary>Conteudo diferente, endereco diferente, duas gravacoes.</summary>
|
|
41
30
|
[Fact]
|
|
42
31
|
public async Task DifferentContentGetsADifferentAddress()
|
|
43
32
|
{
|
|
@@ -52,18 +41,10 @@ public sealed class MediaStoreTests
|
|
|
52
41
|
Assert.Equal(2, blobs.Writes);
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
/// <summary>
|
|
56
|
-
/// O hash e o do CONTEUDO, e nao do conteudo mais o MIME: o mesmo PNG servido como
|
|
57
|
-
/// <c>image/png</c> e como <c>application/octet-stream</c> e o mesmo arquivo.
|
|
58
|
-
/// </summary>
|
|
59
44
|
[Fact]
|
|
60
45
|
public void TheHashIsOfTheContentAlone()
|
|
61
46
|
=> Assert.Equal(MediaHash.Of(Bytes("x").Span), MediaHash.Of(Bytes("x").Span));
|
|
62
47
|
|
|
63
|
-
/// <summary>
|
|
64
|
-
/// O caminho tem dois niveis de prefixo. Um diretorio unico com centenas de milhares de
|
|
65
|
-
/// arquivos degrada listagem em praticamente todo sistema de arquivos.
|
|
66
|
-
/// </summary>
|
|
67
48
|
[Fact]
|
|
68
49
|
public void ThePathIsShardedByTheFirstFourHexDigits()
|
|
69
50
|
{
|
|
@@ -73,10 +54,6 @@ public sealed class MediaStoreTests
|
|
|
73
54
|
Assert.Equal($"{hash.Value[..2]}/{hash.Value[2..4]}/{hash.Value}.png", path);
|
|
74
55
|
}
|
|
75
56
|
|
|
76
|
-
/// <summary>
|
|
77
|
-
/// MIME desconhecido vira <c>.bin</c> e NAO recusa: perder a extensao e cosmetico,
|
|
78
|
-
/// perder os bytes pelos quais se pagou nao e.
|
|
79
|
-
/// </summary>
|
|
80
57
|
[Theory]
|
|
81
58
|
[InlineData("image/png", ".png")]
|
|
82
59
|
[InlineData("image/jpeg", ".jpg")]
|
|
@@ -86,7 +63,6 @@ public sealed class MediaStoreTests
|
|
|
86
63
|
public void UnknownMediaTypesStillGetAPath(string mediaType, string expected)
|
|
87
64
|
=> Assert.Equal(expected, MediaStore.ExtensionFor(mediaType));
|
|
88
65
|
|
|
89
|
-
/// <summary>MIME em branco e defeito de chamada, e grita no lugar em que aconteceu.</summary>
|
|
90
66
|
[Fact]
|
|
91
67
|
public async Task ABlankMediaTypeIsRefused()
|
|
92
68
|
{
|