@polymorphism-tech/morph-spec 8.38.0 → 8.40.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 +19 -8
- 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 +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- 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 +5 -1
- 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 +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -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 +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- 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/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- 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 +3 -5
- 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/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/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- 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
|
@@ -6,14 +6,8 @@ using Xunit;
|
|
|
6
6
|
|
|
7
7
|
namespace Morph.AiKit.Tests;
|
|
8
8
|
|
|
9
|
-
/// <summary>
|
|
10
|
-
/// Canarios do <see cref="ModelRegistry"/> (T8). Tres coisas sao cobradas aqui, e as tres
|
|
11
|
-
/// sao as que o registry v1 nao tinha: <b>protocolo por alias</b>, <b>tarifa por alias</b>
|
|
12
|
-
/// e <b>falha explicita</b> onde antes havia nulo silencioso.
|
|
13
|
-
/// </summary>
|
|
14
9
|
public sealed class ModelRegistryTests
|
|
15
10
|
{
|
|
16
|
-
/// <summary>O exemplar de verdade, embutido do proprio arquivo que o kit publica.</summary>
|
|
17
11
|
private static string ExampleDocument()
|
|
18
12
|
{
|
|
19
13
|
using var stream = typeof(ModelRegistryTests).Assembly
|
|
@@ -33,10 +27,6 @@ public sealed class ModelRegistryTests
|
|
|
33
27
|
}
|
|
34
28
|
""";
|
|
35
29
|
|
|
36
|
-
// ---------------------------------------------------------------------------------
|
|
37
|
-
// O exemplar publicado carrega — e expoe api e pricing por alias
|
|
38
|
-
// ---------------------------------------------------------------------------------
|
|
39
|
-
|
|
40
30
|
[Fact]
|
|
41
31
|
public void ThePublishedExampleLoadsAndExposesApiAndPricingPerAlias()
|
|
42
32
|
{
|
|
@@ -48,11 +38,9 @@ public sealed class ModelRegistryTests
|
|
|
48
38
|
|
|
49
39
|
Assert.Equal("text-default", registry.DefaultAlias);
|
|
50
40
|
|
|
51
|
-
// 'api' omitido cai em Responses — a decisao da casa, verificada e nao suposta.
|
|
52
41
|
Assert.Equal(AgentApi.Responses, registry.Resolve("text-default").Api);
|
|
53
42
|
Assert.Equal(AgentApi.Responses, registry.Resolve("text-heavy").Api);
|
|
54
43
|
|
|
55
|
-
// ... e o MESMO modelo, sob outro alias, pode falar Chat Completions.
|
|
56
44
|
var chat = registry.Resolve("text-heavy-chat");
|
|
57
45
|
Assert.Equal(AgentApi.Chat, chat.Api);
|
|
58
46
|
Assert.Equal(registry.Resolve("text-heavy").Model, chat.Model);
|
|
@@ -62,7 +50,6 @@ public sealed class ModelRegistryTests
|
|
|
62
50
|
Assert.Equal(10.0m, pricing.OutputPerMTok);
|
|
63
51
|
Assert.Equal(0.125m, pricing.CachedInputPerMTok);
|
|
64
52
|
|
|
65
|
-
// cachedInputPerMTok explicitamente nulo = sem desconto declarado (nao "de graca").
|
|
66
53
|
Assert.Null(registry.Resolve("vision").RequirePricing().CachedInputPerMTok);
|
|
67
54
|
|
|
68
55
|
Assert.Equal("http://localhost:11434/v1", registry.Resolve("local-dev").Endpoint);
|
|
@@ -80,10 +67,6 @@ public sealed class ModelRegistryTests
|
|
|
80
67
|
Assert.Contains("reasoning_effort", constraints.Provenance!, StringComparison.Ordinal);
|
|
81
68
|
}
|
|
82
69
|
|
|
83
|
-
// ---------------------------------------------------------------------------------
|
|
84
|
-
// Tarifa ausente e ALTO, nunca nulo silencioso
|
|
85
|
-
// ---------------------------------------------------------------------------------
|
|
86
|
-
|
|
87
70
|
[Fact]
|
|
88
71
|
public void AnAliasWithNeitherPricingNorWaiverIsRefusedAtLoadNamingTheAlias()
|
|
89
72
|
{
|
|
@@ -151,7 +134,7 @@ public sealed class ModelRegistryTests
|
|
|
151
134
|
[Fact]
|
|
152
135
|
public void ATypoInAFieldNameIsRefusedInsteadOfSilentlyIgnored()
|
|
153
136
|
{
|
|
154
|
-
// "priceing"
|
|
137
|
+
// "priceing" e erro de grafia proposital: nao pode ser lido como "sem pricing".
|
|
155
138
|
var document = Document("""
|
|
156
139
|
"digitado-errado": {
|
|
157
140
|
"provider": "openai", "model": "gpt-5.4",
|
|
@@ -165,10 +148,6 @@ public sealed class ModelRegistryTests
|
|
|
165
148
|
Assert.Contains("priceing", error.Message, StringComparison.Ordinal);
|
|
166
149
|
}
|
|
167
150
|
|
|
168
|
-
// ---------------------------------------------------------------------------------
|
|
169
|
-
// api: por alias
|
|
170
|
-
// ---------------------------------------------------------------------------------
|
|
171
|
-
|
|
172
151
|
[Fact]
|
|
173
152
|
public void AnUnknownApiValueIsRefusedListingTheValidOnes()
|
|
174
153
|
{
|
|
@@ -219,10 +198,6 @@ public sealed class ModelRegistryTests
|
|
|
219
198
|
Assert.Contains("'1.0'", error.Message, StringComparison.Ordinal);
|
|
220
199
|
}
|
|
221
200
|
|
|
222
|
-
// ---------------------------------------------------------------------------------
|
|
223
|
-
// Resolucao e memoizacao
|
|
224
|
-
// ---------------------------------------------------------------------------------
|
|
225
|
-
|
|
226
201
|
[Fact]
|
|
227
202
|
public void ResolvingAnUnknownAliasFailsExplicitlyAndListsWhatExists()
|
|
228
203
|
{
|
|
@@ -281,7 +256,7 @@ public sealed class ModelRegistryTests
|
|
|
281
256
|
Assert.True(client.IsDisposed);
|
|
282
257
|
Assert.Throws<ObjectDisposedException>(() => registry.GetChatClient("text-default"));
|
|
283
258
|
|
|
284
|
-
registry.Dispose();
|
|
259
|
+
registry.Dispose();
|
|
285
260
|
}
|
|
286
261
|
|
|
287
262
|
[Fact]
|
|
@@ -312,16 +287,12 @@ public sealed class ModelRegistryTests
|
|
|
312
287
|
Assert.IsAssignableFrom<System.Text.Json.JsonException>(error.InnerException);
|
|
313
288
|
}
|
|
314
289
|
|
|
315
|
-
// ---------------------------------------------------------------------------------
|
|
316
|
-
// Tarifa: a conta
|
|
317
|
-
// ---------------------------------------------------------------------------------
|
|
318
|
-
|
|
319
290
|
[Fact]
|
|
320
291
|
public void CostIsTheTariffTimesTheTokensDividedByAMillion()
|
|
321
292
|
{
|
|
322
293
|
var pricing = new ModelPricing(InputPerMTok: 1.25m, OutputPerMTok: 10m, CachedInputPerMTok: null);
|
|
323
294
|
|
|
324
|
-
//
|
|
295
|
+
// 1M de entrada * 1.25 + 100k de saida * 10.0 = 1.25 + 1.00 USD.
|
|
325
296
|
Assert.Equal(2.25m, pricing.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 100_000));
|
|
326
297
|
Assert.Equal(0m, pricing.ComputeCostUsd(0, 0));
|
|
327
298
|
}
|
|
@@ -332,13 +303,12 @@ public sealed class ModelRegistryTests
|
|
|
332
303
|
var comCache = new ModelPricing(1.25m, 10m, CachedInputPerMTok: 0.125m);
|
|
333
304
|
var semCache = comCache with { CachedInputPerMTok = null };
|
|
334
305
|
|
|
335
|
-
// 1M
|
|
336
|
-
// com tarifa de cache: 200k * 1.25 + 800k * 0.125 = 0.25 + 0.10 = 0.35
|
|
306
|
+
// 800k dos 1M vieram do cache: 200k * 1.25 + 800k * 0.125 = 0.25 + 0.10 = 0.35.
|
|
337
307
|
Assert.Equal(
|
|
338
308
|
0.35m,
|
|
339
309
|
comCache.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 0, cachedInputTokens: 800_000));
|
|
340
310
|
|
|
341
|
-
//
|
|
311
|
+
// Sem tarifa de cache, a entrada inteira e cobrada cheia.
|
|
342
312
|
Assert.Equal(
|
|
343
313
|
1.25m,
|
|
344
314
|
semCache.ComputeCostUsd(inputTokens: 1_000_000, outputTokens: 0, cachedInputTokens: 800_000));
|
|
@@ -355,36 +325,28 @@ public sealed class ModelRegistryTests
|
|
|
355
325
|
() => pricing.ComputeCostUsd(inputTokens: -1, outputTokens: 0));
|
|
356
326
|
}
|
|
357
327
|
|
|
358
|
-
// ---------------------------------------------------------------------------------
|
|
359
|
-
// Limites por alias — o limite REAL, nao o folclore
|
|
360
|
-
// ---------------------------------------------------------------------------------
|
|
361
|
-
|
|
362
328
|
[Fact]
|
|
363
329
|
public void TheChatToolLimitFiresOnlyWithAllFourFactsTogether()
|
|
364
330
|
{
|
|
365
331
|
using var registry = ModelRegistry.FromJson(ExampleDocument(), new FakeProviderClientFactory());
|
|
366
332
|
var alias = registry.Resolve("text-heavy-chat");
|
|
367
333
|
|
|
368
|
-
//
|
|
334
|
+
// Os quatro fatos juntos: Chat + tools + effort enviado + effort != none.
|
|
369
335
|
var violado = alias.CheckRequest(
|
|
370
336
|
AgentApi.Chat, hasTools: true, reasoningEffort: ReasoningEffort.High, temperatureIsSet: false);
|
|
371
337
|
|
|
372
338
|
Assert.False(violado.IsSatisfied);
|
|
373
339
|
Assert.Contains("reasoning effort", Assert.Single(violado.Violations), StringComparison.Ordinal);
|
|
374
340
|
|
|
375
|
-
// mesmo pedido, na Responses API: sem violacao
|
|
376
341
|
Assert.True(alias.CheckRequest(
|
|
377
342
|
AgentApi.Responses, hasTools: true, ReasoningEffort.High, temperatureIsSet: false).IsSatisfied);
|
|
378
343
|
|
|
379
|
-
// sem tools: sem violacao
|
|
380
344
|
Assert.True(alias.CheckRequest(
|
|
381
345
|
AgentApi.Chat, hasTools: false, ReasoningEffort.High, temperatureIsSet: false).IsSatisfied);
|
|
382
346
|
|
|
383
|
-
// effort 'none': e exatamente o que a documentacao permite
|
|
384
347
|
Assert.True(alias.CheckRequest(
|
|
385
348
|
AgentApi.Chat, hasTools: true, ReasoningEffort.None, temperatureIsSet: false).IsSatisfied);
|
|
386
349
|
|
|
387
|
-
// effort nao enviado: o default do provider vale, e o kit NAO afirma saber qual e
|
|
388
350
|
Assert.True(alias.CheckRequest(
|
|
389
351
|
AgentApi.Chat, hasTools: true, reasoningEffort: null, temperatureIsSet: false).IsSatisfied);
|
|
390
352
|
}
|
|
@@ -404,10 +366,6 @@ public sealed class ModelRegistryTests
|
|
|
404
366
|
[Fact]
|
|
405
367
|
public void TheTemperatureLimitIsPerAliasAndOnlyBitesWhenTemperatureIsSent()
|
|
406
368
|
{
|
|
407
|
-
// A afirmacao "temperatura e obsoleta para modelo de raciocinio" NAO foi confirmada
|
|
408
|
-
// em fonte primaria (issue #93). Por isso ela e uma flag por alias com procedencia
|
|
409
|
-
// escrita — nunca uma regra universal no codigo. Aqui a procedencia diz, por extenso,
|
|
410
|
-
// que o exemplo e sintetico.
|
|
411
369
|
var document = Document("""
|
|
412
370
|
"sem-temperatura": {
|
|
413
371
|
"provider": "openai", "model": "modelo-hipotetico",
|
|
@@ -4,19 +4,7 @@ using Xunit;
|
|
|
4
4
|
|
|
5
5
|
namespace Morph.AiKit.Tests;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
/// Canario do esqueleto: prova que o harness de teste EXECUTA de verdade e que o
|
|
9
|
-
/// assembly do kit foi compilado no TFM do pin. Nao testa comportamento — os testes
|
|
10
|
-
/// de AgentFactory, ModelRegistry, middlewares, ConversationStore e DeltaIngest sao
|
|
11
|
-
/// das tasks T7-T14.
|
|
12
|
-
///
|
|
13
|
-
/// Por que ele existe em vez de um projeto de teste literalmente vazio: um projeto
|
|
14
|
-
/// sem nenhum teste NAO sai 0. Microsoft.Testing.Platform devolve exit code 8
|
|
15
|
-
/// ("zero tests ran") e o VSTest antigo devolvia 1 com "no test is available".
|
|
16
|
-
/// Um esqueleto que so "compila" e um CI verde que nunca executou nada — e um verde
|
|
17
|
-
/// desses e indistinguivel de um verde real, que e exatamente o silencio que esta
|
|
18
|
-
/// feature existe para quebrar.
|
|
19
|
-
/// </summary>
|
|
7
|
+
// Um projeto sem teste nenhum nao sai 0 (o Microsoft.Testing.Platform sai 8): este caso prova que o harness executa.
|
|
20
8
|
public sealed class SkeletonSmokeTests
|
|
21
9
|
{
|
|
22
10
|
[Fact]
|
|
@@ -9,14 +9,7 @@ using Xunit;
|
|
|
9
9
|
|
|
10
10
|
namespace Morph.AiKit.Tests;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
/// Canarios da telemetria (T12). O arquivo inteiro mede <b>spans</b>, com
|
|
14
|
-
/// <see cref="ActivityListener"/> — nao propriedades de configuracao. Um guard contra span
|
|
15
|
-
/// duplicado so pode ser provado contando spans.
|
|
16
|
-
/// </summary>
|
|
17
|
-
/// <remarks>
|
|
18
|
-
/// Cada caso usa um <c>SourceName</c> unico, para que um teste nunca conte o span do outro.
|
|
19
|
-
/// </remarks>
|
|
12
|
+
// Cada caso usa um SourceName proprio, para nunca contar o span de outro teste.
|
|
20
13
|
public sealed class TelemetryTests
|
|
21
14
|
{
|
|
22
15
|
private const string Document = """
|
|
@@ -42,15 +35,6 @@ public sealed class TelemetryTests
|
|
|
42
35
|
Options = options ?? new AgentSpecOptions(),
|
|
43
36
|
};
|
|
44
37
|
|
|
45
|
-
// ---------------------------------------------------------------------------------
|
|
46
|
-
// O caso normal: um span de agente, um span de chat
|
|
47
|
-
// ---------------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
/// <summary>
|
|
50
|
-
/// A forma que as GenAI conventions descrevem: <c>invoke_agent</c> como raiz e
|
|
51
|
-
/// <c>chat</c> como filho. Este teste tambem e a prova de que a escolha da T10 de montar
|
|
52
|
-
/// o proprio pipeline nao custou o span de chat — ele so existe porque a T12 o coloca.
|
|
53
|
-
/// </summary>
|
|
54
38
|
[Fact]
|
|
55
39
|
public async Task AnInstrumentedAgentEmitsOneInvokeAgentSpanAndOneChatSpan()
|
|
56
40
|
{
|
|
@@ -66,14 +50,6 @@ public sealed class TelemetryTests
|
|
|
66
50
|
Assert.NotNull(Single(spans, "chat").Parent);
|
|
67
51
|
}
|
|
68
52
|
|
|
69
|
-
// ---------------------------------------------------------------------------------
|
|
70
|
-
// O guard — as duas metades da mesma afirmacao
|
|
71
|
-
// ---------------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
/// <summary>
|
|
74
|
-
/// <b>O guard, no default.</b> Um pipeline que ja carrega instrumentacao (o caso real:
|
|
75
|
-
/// um <c>IChatClient</c> que veio do DI ja instrumentado) continua com UM span de chat.
|
|
76
|
-
/// </summary>
|
|
77
53
|
[Fact]
|
|
78
54
|
public async Task TheDefaultDoesNotAddASecondChatSpanOverAnAlreadyInstrumentedPipeline()
|
|
79
55
|
{
|
|
@@ -87,11 +63,6 @@ public sealed class TelemetryTests
|
|
|
87
63
|
Assert.Equal(1, CountOf(spans, "chat"));
|
|
88
64
|
}
|
|
89
65
|
|
|
90
|
-
/// <summary>
|
|
91
|
-
/// <b>O guard, desligado.</b> Forcar <c>InstrumentChatClient = true</c> sobre o MESMO
|
|
92
|
-
/// pipeline ja instrumentado produz o par duplicado. E o canario que documenta o risco:
|
|
93
|
-
/// sem o ramo automatico, este e o resultado.
|
|
94
|
-
/// </summary>
|
|
95
66
|
[Fact]
|
|
96
67
|
public async Task ForcingInstrumentChatClientProducesTheDuplicateChatSpan()
|
|
97
68
|
{
|
|
@@ -105,10 +76,6 @@ public sealed class TelemetryTests
|
|
|
105
76
|
Assert.Equal(2, CountOf(spans, "chat"));
|
|
106
77
|
}
|
|
107
78
|
|
|
108
|
-
/// <summary>
|
|
109
|
-
/// O mesmo guard no nivel do agente: aplicar a telemetria duas vezes no
|
|
110
|
-
/// <see cref="AIAgentBuilder"/> nao produz dois <c>invoke_agent</c>.
|
|
111
|
-
/// </summary>
|
|
112
79
|
[Fact]
|
|
113
80
|
public async Task TheAgentGuardKeepsASingleInvokeAgentSpanWhenTelemetryIsAppliedTwice()
|
|
114
81
|
{
|
|
@@ -124,12 +91,6 @@ public sealed class TelemetryTests
|
|
|
124
91
|
Assert.Equal(1, CountOf(spans, "invoke_agent"));
|
|
125
92
|
}
|
|
126
93
|
|
|
127
|
-
/// <summary>
|
|
128
|
-
/// <c>false</c> nao e o mesmo que o default. Com a <c>AgentFactory</c> — que monta o
|
|
129
|
-
/// proprio pipeline — desligar a instrumentacao de chat deixa a invocacao com o span do
|
|
130
|
-
/// agente e <b>nenhum</b> span de chat. O teste existe para que essa consequencia seja
|
|
131
|
-
/// um fato escrito, e nao uma surpresa em producao.
|
|
132
|
-
/// </summary>
|
|
133
94
|
[Fact]
|
|
134
95
|
public async Task TurningTheChatInstrumentationOffLeavesTheAgentSpanAndNoChatSpan()
|
|
135
96
|
{
|
|
@@ -143,14 +104,6 @@ public sealed class TelemetryTests
|
|
|
143
104
|
Assert.Equal(0, CountOf(spans, "chat"));
|
|
144
105
|
}
|
|
145
106
|
|
|
146
|
-
// ---------------------------------------------------------------------------------
|
|
147
|
-
// Dado sensivel
|
|
148
|
-
// ---------------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
/// <summary>
|
|
151
|
-
/// Conteudo sensivel nasce desligado nos dois niveis — o standard exige <c>false</c>
|
|
152
|
-
/// fora de dev/staging, porque prompt e resposta sao PII do cliente.
|
|
153
|
-
/// </summary>
|
|
154
107
|
[Fact]
|
|
155
108
|
public void SensitiveDataIsOffByDefaultAtBothLevels()
|
|
156
109
|
{
|
|
@@ -163,11 +116,6 @@ public sealed class TelemetryTests
|
|
|
163
116
|
agent.GetService(typeof(OpenTelemetryChatClient))).EnableSensitiveData);
|
|
164
117
|
}
|
|
165
118
|
|
|
166
|
-
/// <summary>
|
|
167
|
-
/// A spec pode ligar o dado sensivel para UM agente sem mexer na configuracao global —
|
|
168
|
-
/// e o destino que a T7 declarou para <c>EnableSensitiveTelemetry</c>. Nulo na spec
|
|
169
|
-
/// ("nao opinei") preserva o valor global; preenchido, vence.
|
|
170
|
-
/// </summary>
|
|
171
119
|
[Fact]
|
|
172
120
|
public void TheSpecCanOverrideSensitiveDataForASingleAgent()
|
|
173
121
|
{
|
|
@@ -183,25 +131,12 @@ public sealed class TelemetryTests
|
|
|
183
131
|
loud.GetService(typeof(OpenTelemetryChatClient))).EnableSensitiveData);
|
|
184
132
|
}
|
|
185
133
|
|
|
186
|
-
// ---------------------------------------------------------------------------------
|
|
187
|
-
// Configuracao invalida
|
|
188
|
-
// ---------------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
/// <summary>
|
|
191
|
-
/// Source em branco e recusado na construcao: um <c>ActivitySource</c> sem nome nao pode
|
|
192
|
-
/// ser registrado em <c>AddSource(...)</c>, e a telemetria sumiria em silencio.
|
|
193
|
-
/// </summary>
|
|
194
134
|
[Fact]
|
|
195
135
|
public void ABlankSourceNameIsRefusedAtConstruction()
|
|
196
136
|
=> Assert.Throws<ArgumentException>(() => new MorphTelemetryOptions { SourceName = " " });
|
|
197
137
|
|
|
198
|
-
// ---------------------------------------------------------------------------------
|
|
199
|
-
// Apoio dos testes
|
|
200
|
-
// ---------------------------------------------------------------------------------
|
|
201
|
-
|
|
202
138
|
private static string NewSourceName() => "morph.aikit.test." + Guid.CreateVersion7().ToString("N");
|
|
203
139
|
|
|
204
|
-
/// <summary>Um pipeline que ja chega instrumentado — o caso real do cliente vindo do DI.</summary>
|
|
205
140
|
private static IChatClient AlreadyInstrumented(string sourceName)
|
|
206
141
|
=> new FakeChatClient()
|
|
207
142
|
.EnqueueText("pronto")
|
|
@@ -219,11 +154,7 @@ public sealed class TelemetryTests
|
|
|
219
154
|
return factory.Create(spec ?? Spec());
|
|
220
155
|
}
|
|
221
156
|
|
|
222
|
-
|
|
223
|
-
/// Coleta os spans de UMA invocacao. O listener e registrado antes de o agente existir e
|
|
224
|
-
/// removido no fim — telemetria e estado global do processo, e um listener vazado
|
|
225
|
-
/// contaminaria os outros casos.
|
|
226
|
-
/// </summary>
|
|
157
|
+
// Telemetria e estado global do processo: o listener entra antes do agente e sai no fim.
|
|
227
158
|
private static async Task<IReadOnlyList<Activity>> CollectAsync(string sourceName, Func<AIAgent> build)
|
|
228
159
|
{
|
|
229
160
|
var spans = new List<Activity>();
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs
CHANGED
|
@@ -7,16 +7,10 @@ using Xunit;
|
|
|
7
7
|
|
|
8
8
|
namespace Morph.AiKit.Tests;
|
|
9
9
|
|
|
10
|
-
/// <summary>
|
|
11
|
-
/// Canarios do <see cref="UsageCostMiddleware"/> (T11). A afirmacao central da task e uma
|
|
12
|
-
/// so: <b><c>cost_usd</c> nao sai nulo, e o zero nunca e ambiguo</b> — ou e medicao, ou e
|
|
13
|
-
/// dispensa escrita, ou e lacuna declarada.
|
|
14
|
-
/// </summary>
|
|
15
10
|
public sealed class UsageCostMiddlewareTests
|
|
16
11
|
{
|
|
17
12
|
private static readonly DateTimeOffset FixedNow = new(2026, 9, 8, 12, 0, 0, TimeSpan.Zero);
|
|
18
13
|
|
|
19
|
-
/// <summary>Tarifa ilustrativa com desconto de cache — a que exercita a aritmetica inteira.</summary>
|
|
20
14
|
private static ModelPricing Tariff()
|
|
21
15
|
=> new(InputPerMTok: 1.25m, OutputPerMTok: 10.0m, CachedInputPerMTok: 0.125m);
|
|
22
16
|
|
|
@@ -42,22 +36,11 @@ public sealed class UsageCostMiddlewareTests
|
|
|
42
36
|
|
|
43
37
|
var sink = new FakeUsageCostSink();
|
|
44
38
|
|
|
45
|
-
// A extensao publica e o caminho de registro: se ela quebrar, o teste quebra junto.
|
|
46
39
|
var client = inner.AsBuilder().UseUsageCost(alias, sink, new FakeClock(FixedNow)).Build();
|
|
47
40
|
|
|
48
41
|
return (client, inner, sink);
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
// ---------------------------------------------------------------------------------
|
|
52
|
-
// doneCriteria 1: Usage conhecido + ModelPricing conhecido => CostUsd EXATO
|
|
53
|
-
// ---------------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
/// <summary>
|
|
56
|
-
/// O valor esperado esta escrito a mao, com a semantica correta de token cacheado:
|
|
57
|
-
/// <b>cacheado e SUBCONJUNTO da entrada</b>. 1000 de entrada com 400 servidos do cache
|
|
58
|
-
/// custam 600 na tarifa cheia mais 400 na de cache — nunca 1000 mais 400. E por isso que
|
|
59
|
-
/// este caso, e nao um sem cache, e o que denuncia uma segunda aritmetica.
|
|
60
|
-
/// </summary>
|
|
61
44
|
[Fact]
|
|
62
45
|
public async Task AKnownUsageAgainstAKnownTariffProducesTheExactCostInUsd()
|
|
63
46
|
{
|
|
@@ -83,13 +66,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
83
66
|
Assert.Equal(FixedNow, record.At);
|
|
84
67
|
}
|
|
85
68
|
|
|
86
|
-
/// <summary>
|
|
87
|
-
/// O canario que torna "nao existe uma segunda conta do custo" verificavel em vez de
|
|
88
|
-
/// prometido: o custo publicado tem de ser <b>identico</b> ao de
|
|
89
|
-
/// <see cref="ModelPricing.ComputeCostUsd"/> — inclusive nos casos que separam as duas
|
|
90
|
-
/// formulas plausiveis (cacheado sem tarifa de cache, cacheado igual a entrada, saida
|
|
91
|
-
/// zerada).
|
|
92
|
-
/// </summary>
|
|
93
69
|
[Theory]
|
|
94
70
|
[InlineData(1000, 200, 400L)]
|
|
95
71
|
[InlineData(1000, 200, null)]
|
|
@@ -111,11 +87,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
111
87
|
Assert.Equal(pricing.ComputeCostUsd(input, output, cached), sink.Records[0].CostUsd);
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
/// <summary>
|
|
115
|
-
/// Tarifa sem desconto de cache declarado: nulo em <c>CachedInputPerMTok</c> significa
|
|
116
|
-
/// "sem desconto", <b>nunca</b> "de graca" — a entrada inteira e cobrada cheia mesmo com
|
|
117
|
-
/// tokens vindos do cache.
|
|
118
|
-
/// </summary>
|
|
119
90
|
[Fact]
|
|
120
91
|
public async Task WithoutADeclaredCacheTariffTheWholeInputIsChargedAsInput()
|
|
121
92
|
{
|
|
@@ -127,19 +98,10 @@ public sealed class UsageCostMiddlewareTests
|
|
|
127
98
|
[new ChatMessage(ChatRole.User, "oi")],
|
|
128
99
|
cancellationToken: TestContext.Current.CancellationToken);
|
|
129
100
|
|
|
130
|
-
// (1000 * 1.25) + (200 * 10.0) = 3250 por 1M tokens
|
|
101
|
+
// (1000 * 1.25) + (200 * 10.0) = 3250 por 1M tokens: o cacheado nao rende desconto.
|
|
131
102
|
Assert.Equal(0.00325m, sink.Records[0].CostUsd);
|
|
132
103
|
}
|
|
133
104
|
|
|
134
|
-
// ---------------------------------------------------------------------------------
|
|
135
|
-
// doneCriteria 2: alias sem Pricing => CostUsd == 0, ausencia sinalizada, SEM excecao
|
|
136
|
-
// ---------------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
/// <summary>
|
|
139
|
-
/// Alias sem tarifa e caminho previsto, nao erro. E o zero nao e anonimo: o motivo
|
|
140
|
-
/// escrito no <c>pricingWaiver</c> do registry viaja no record. Nao ha excecao — e nao
|
|
141
|
-
/// ha nulo em <c>CostUsd</c>.
|
|
142
|
-
/// </summary>
|
|
143
105
|
[Fact]
|
|
144
106
|
public async Task AnAliasWithoutATariffRecordsTokensWithZeroCostAndSaysWhy()
|
|
145
107
|
{
|
|
@@ -156,19 +118,14 @@ public sealed class UsageCostMiddlewareTests
|
|
|
156
118
|
|
|
157
119
|
var record = Assert.Single(sink.Records);
|
|
158
120
|
|
|
159
|
-
Assert.Equal(0m, record.CostUsd);
|
|
121
|
+
Assert.Equal(0m, record.CostUsd);
|
|
160
122
|
Assert.Equal(UsageCostStatus.Waived, record.Status);
|
|
161
|
-
Assert.False(record.IsMeasuredCost);
|
|
123
|
+
Assert.False(record.IsMeasuredCost);
|
|
162
124
|
Assert.Equal(alias.PricingWaiver, record.PricingWaiver);
|
|
163
|
-
Assert.Equal(900, record.InputTokens);
|
|
125
|
+
Assert.Equal(900, record.InputTokens);
|
|
164
126
|
Assert.Equal(100, record.OutputTokens);
|
|
165
127
|
}
|
|
166
128
|
|
|
167
|
-
/// <summary>
|
|
168
|
-
/// A regra que sustenta a anterior mora no registry (T8): um alias sem tarifa <b>e</b>
|
|
169
|
-
/// sem dispensa nao carrega. Por isso um record <c>Waived</c> sempre tem motivo — a
|
|
170
|
-
/// ausencia por esquecimento morre antes de chegar ao middleware.
|
|
171
|
-
/// </summary>
|
|
172
129
|
[Fact]
|
|
173
130
|
public void AWaivedRecordAlwaysHasAReasonBecauseTheRegistryRefusesAnAliasWithNeither()
|
|
174
131
|
{
|
|
@@ -180,13 +137,9 @@ public sealed class UsageCostMiddlewareTests
|
|
|
180
137
|
var refusal = Assert.Throws<ModelRegistryException>(() => ModelAlias.ValidatePricingDecision(orphan));
|
|
181
138
|
Assert.Contains("sem-decisao", refusal.Message, StringComparison.Ordinal);
|
|
182
139
|
|
|
183
|
-
ModelAlias.ValidatePricingDecision(WaivedAlias());
|
|
140
|
+
ModelAlias.ValidatePricingDecision(WaivedAlias());
|
|
184
141
|
}
|
|
185
142
|
|
|
186
|
-
/// <summary>
|
|
187
|
-
/// A soma so junta o que foi medido. Uma volta tarifada mais uma dispensada somam o
|
|
188
|
-
/// custo da primeira — o zero da dispensa nao "abaixa a media" nem some do relatorio.
|
|
189
|
-
/// </summary>
|
|
190
143
|
[Fact]
|
|
191
144
|
public async Task OnlyMeasuredCostsEnterTheSum()
|
|
192
145
|
{
|
|
@@ -210,15 +163,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
210
163
|
Assert.Equal(0.0028m, sink.MeasuredTotalUsd);
|
|
211
164
|
}
|
|
212
165
|
|
|
213
|
-
// ---------------------------------------------------------------------------------
|
|
214
|
-
// O terceiro zero: o provider nao reportou uso
|
|
215
|
-
// ---------------------------------------------------------------------------------
|
|
216
|
-
|
|
217
|
-
/// <summary>
|
|
218
|
-
/// Resposta sem <c>Usage</c>: a linha sai marcada como lacuna. Nao publicar nada faria o
|
|
219
|
-
/// custo desaparecer do relatorio, que e a mesma dor por outro angulo; publicar como
|
|
220
|
-
/// <c>Computed</c> mentiria que a volta custou zero.
|
|
221
|
-
/// </summary>
|
|
222
166
|
[Fact]
|
|
223
167
|
public async Task AResponseWithoutUsageIsRecordedAsAGapNotAsFree()
|
|
224
168
|
{
|
|
@@ -236,11 +180,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
236
180
|
Assert.Equal(0m, sink.MeasuredTotalUsd);
|
|
237
181
|
}
|
|
238
182
|
|
|
239
|
-
/// <summary>
|
|
240
|
-
/// <c>cached > input</c> e o provider se contradizendo. A conta do T8 recusa esse par
|
|
241
|
-
/// com excecao — e recusar aqui derrubaria a resposta do usuario por causa de um
|
|
242
|
-
/// contador. O middleware degrada para lacuna declarada, sem lancar.
|
|
243
|
-
/// </summary>
|
|
244
183
|
[Fact]
|
|
245
184
|
public async Task InconsistentTokenCountsBecomeADeclaredGapInsteadOfAThrownRequest()
|
|
246
185
|
{
|
|
@@ -259,26 +198,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
259
198
|
Assert.Equal(UsageCostStatus.UsageMissing, Assert.Single(sink.Records).Status);
|
|
260
199
|
}
|
|
261
200
|
|
|
262
|
-
// ---------------------------------------------------------------------------------
|
|
263
|
-
// Uma linha por volta ao modelo — inclusive no streaming
|
|
264
|
-
// ---------------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
/// <summary>
|
|
267
|
-
/// Duas <b>chamadas</b> a <c>GetResponseAsync</c> publicam <b>duas</b> linhas: uma por
|
|
268
|
-
/// ida ao modelo, cada uma com o seu proprio numero.
|
|
269
|
-
/// </summary>
|
|
270
|
-
/// <remarks>
|
|
271
|
-
/// O que este teste prova, e so isto: <b>uma linha por chamada</b>, sem acumulo de
|
|
272
|
-
/// estado entre chamadas. Ele NAO prova nada sobre a posicao do middleware no pipeline
|
|
273
|
-
/// — sao duas chamadas separadas, nao uma chamada com volta de tool, e aqui o
|
|
274
|
-
/// middleware e montado sozinho, sem <c>FunctionInvokingChatClient</c> nenhum. A
|
|
275
|
-
/// posicao real e o oposto do que uma leitura apressada deste teste sugeriria: o
|
|
276
|
-
/// middleware fica <b>ACIMA</b> da invocacao de funcoes, e quem prova isso e
|
|
277
|
-
/// <c>AgentFactoryTests.ThePipelineOrderIsUsageCostAboveFunctionInvocationAndTelemetryBelowIt</c>
|
|
278
|
-
/// (com <c>...SeesTheUsageOfEveryToolRoundSummedOnce</c> mostrando o uso ja somado das
|
|
279
|
-
/// duas voltas). Acima do FICC, uma conversa com tool call e <b>uma</b> chamada com o
|
|
280
|
-
/// uso ja totalizado — nao duas.
|
|
281
|
-
/// </remarks>
|
|
282
201
|
[Fact]
|
|
283
202
|
public async Task EveryTripToTheModelPublishesItsOwnLine()
|
|
284
203
|
{
|
|
@@ -301,10 +220,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
301
220
|
Assert.Equal(0.0035m, sink.Records[1].CostUsd); // (2000*1.25 + 100*10) / 1M
|
|
302
221
|
}
|
|
303
222
|
|
|
304
|
-
/// <summary>
|
|
305
|
-
/// No streaming o uso chega como <c>UsageContent</c> avulso. A linha sai <b>uma</b> vez,
|
|
306
|
-
/// ao fim da enumeracao, com o mesmo numero do caminho nao-streaming.
|
|
307
|
-
/// </summary>
|
|
308
223
|
[Fact]
|
|
309
224
|
public async Task StreamingPublishesOneLineAtTheEndWithTheSameNumber()
|
|
310
225
|
{
|
|
@@ -325,11 +240,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
325
240
|
Assert.Equal(0.0028m, Assert.Single(sink.Records).CostUsd);
|
|
326
241
|
}
|
|
327
242
|
|
|
328
|
-
// ---------------------------------------------------------------------------------
|
|
329
|
-
// Higiene de construcao
|
|
330
|
-
// ---------------------------------------------------------------------------------
|
|
331
|
-
|
|
332
|
-
/// <summary>Alias ou sink nulos sao defeito de composicao e falham na construcao.</summary>
|
|
333
243
|
[Fact]
|
|
334
244
|
public void TheMiddlewareRefusesToBeBuiltWithoutAnAliasOrASink()
|
|
335
245
|
{
|
|
@@ -342,11 +252,6 @@ public sealed class UsageCostMiddlewareTests
|
|
|
342
252
|
() => new UsageCostMiddleware(inner, PricedAlias(), null!));
|
|
343
253
|
}
|
|
344
254
|
|
|
345
|
-
/// <summary>
|
|
346
|
-
/// O instante do record vem do <see cref="TimeProvider"/> injetado, nao do relogio do
|
|
347
|
-
/// processo — sem isso o campo <c>At</c> seria intestavel (standard
|
|
348
|
-
/// <c>backend-dotnet-testing</c>).
|
|
349
|
-
/// </summary>
|
|
350
255
|
[Fact]
|
|
351
256
|
public async Task TheTimestampComesFromTheInjectedClock()
|
|
352
257
|
{
|
|
@@ -1,43 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
ENV PORT=3000
|
|
41
|
-
ENV HOSTNAME="0.0.0.0"
|
|
42
|
-
|
|
43
|
-
CMD ["node", "server.js"]
|
|
1
|
+
FROM node:20-alpine AS deps
|
|
2
|
+
RUN apk add --no-cache libc6-compat
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package.json package-lock.json* ./
|
|
6
|
+
RUN npm ci --only=production && npm cache clean --force
|
|
7
|
+
|
|
8
|
+
FROM node:20-alpine AS builder
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
12
|
+
COPY . .
|
|
13
|
+
|
|
14
|
+
# NEXT_PUBLIC_ vars are inlined by next build, so they must exist at build time
|
|
15
|
+
ARG NEXT_PUBLIC_APP_URL
|
|
16
|
+
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
|
|
17
|
+
|
|
18
|
+
RUN npm run build
|
|
19
|
+
|
|
20
|
+
FROM node:20-alpine AS runner
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
ENV NODE_ENV=production
|
|
24
|
+
ENV NEXT_TELEMETRY_DISABLED=1
|
|
25
|
+
|
|
26
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
27
|
+
RUN adduser --system --uid 1001 nextjs
|
|
28
|
+
|
|
29
|
+
COPY --from=builder /app/public ./public
|
|
30
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
31
|
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
32
|
+
|
|
33
|
+
USER nextjs
|
|
34
|
+
|
|
35
|
+
EXPOSE 3000
|
|
36
|
+
ENV PORT=3000
|
|
37
|
+
ENV HOSTNAME="0.0.0.0"
|
|
38
|
+
|
|
39
|
+
CMD ["node", "server.js"]
|