@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.
Files changed (227) hide show
  1. package/CLAUDE.md +19 -8
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -2
  8. package/framework/MORPH.md +1 -1
  9. package/framework/agents.json +36 -25
  10. package/framework/docs/CLI.md +2 -2
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/mcp/registry.json +33 -2
  14. package/framework/rules/infrastructure-standards.md +0 -1
  15. package/framework/rules/morph-workflow.md +9 -4
  16. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  17. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  18. package/framework/skills/core/morph-apply/SKILL.md +5 -1
  19. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  20. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  21. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  22. package/framework/skills/core/morph-implement/SKILL.md +6 -1
  23. package/framework/skills/core/morph-media/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +1 -1
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/skills/core/morph-uiux/SKILL.md +6 -5
  30. package/framework/standards/STANDARDS.json +3 -3
  31. package/framework/standards/ai-agents/context-providers.md +16 -26
  32. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  33. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  34. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  35. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  36. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  37. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  38. package/framework/standards/architecture/code-comments.md +52 -16
  39. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  40. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  41. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  42. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  43. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  44. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  45. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  46. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  47. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  48. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  49. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  50. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  51. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  52. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  53. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  54. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  55. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  56. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  57. package/framework/templates/code/dotnet/test.cs +2 -87
  58. package/framework/templates/code/typescript/contracts.ts +140 -167
  59. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  60. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  61. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  62. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  63. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  64. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  65. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  66. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  110. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  111. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  112. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  113. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  114. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  115. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  116. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  117. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  142. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  143. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  144. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  145. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  146. package/framework/templates/integrations/asaas-client.cs +332 -387
  147. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  148. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  149. package/framework/templates/project/validate.js +97 -120
  150. package/package.json +2 -2
  151. package/src/commands/dashboard/dashboard.js +80 -53
  152. package/src/commands/mcp/mcp-setup.js +37 -9
  153. package/src/commands/project/doctor.js +3 -5
  154. package/src/commands/project/init.js +6 -3
  155. package/src/commands/project/install-plugin-cmd.js +1 -1
  156. package/src/commands/project/update.js +5 -0
  157. package/src/dashboard/app/App.jsx +80 -16
  158. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  159. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  160. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  161. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  162. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  163. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  164. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  165. package/src/dashboard/app/components/Markdown.jsx +47 -0
  166. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  167. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  168. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  169. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  170. package/src/dashboard/app/hooks/usePref.js +38 -0
  171. package/src/dashboard/app/lib/format.js +69 -0
  172. package/src/dashboard/app/lib/phases.js +56 -0
  173. package/src/dashboard/app/lib/project.js +50 -0
  174. package/src/dashboard/app/pages/Activity.jsx +24 -0
  175. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  176. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  177. package/src/dashboard/app/pages/Features.jsx +400 -0
  178. package/src/dashboard/app/pages/Guide.jsx +146 -0
  179. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  180. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  181. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  182. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  183. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  184. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  185. package/src/dashboard/app/pages/Standards.jsx +407 -332
  186. package/src/dashboard/app/styles/index.css +187 -47
  187. package/src/dashboard/index.html +14 -12
  188. package/src/dashboard/package.json +32 -24
  189. package/src/dashboard/scanner/activity-scanner.js +351 -0
  190. package/src/dashboard/scanner/agents-scanner.js +59 -1
  191. package/src/dashboard/scanner/docs-scanner.js +59 -0
  192. package/src/dashboard/scanner/feature-status.js +10 -0
  193. package/src/dashboard/scanner/features-scanner.js +360 -0
  194. package/src/dashboard/scanner/index.js +14 -7
  195. package/src/dashboard/scanner/phases-scanner.js +14 -15
  196. package/src/dashboard/scanner/portfolio.js +153 -0
  197. package/src/dashboard/scanner/project-api.js +195 -0
  198. package/src/dashboard/scanner/rules-scanner.js +42 -37
  199. package/src/dashboard/scanner/skills-scanner.js +105 -43
  200. package/src/dashboard/scanner/standards-scanner.js +2 -0
  201. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  202. package/src/lib/detectors/claude-config-detector.js +2 -7
  203. package/src/lib/detectors/index.js +5 -1
  204. package/src/lib/evaluator-report.js +22 -0
  205. package/src/lib/generators/settings-generator.js +208 -209
  206. package/src/lib/installers/mcp-installer.js +724 -719
  207. package/src/lib/installers/memory-installer.js +1 -2
  208. package/src/lib/projects/registry.js +154 -0
  209. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  210. package/src/scripts/install-plugin.js +1 -1
  211. package/src/scripts/setup-infra.js +2 -4
  212. package/src/utils/agents-installer.js +57 -12
  213. package/src/utils/banner.js +1 -1
  214. package/src/commands/index.js +0 -13
  215. package/src/commands/project/checkpoint.js +0 -209
  216. package/src/core/state/phase-state-machine.js +0 -197
  217. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  218. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  219. package/src/dashboard/scanner/squads-scanner.js +0 -13
  220. package/src/dashboard/scanner/state-scanner.js +0 -85
  221. package/src/lib/generators/index.js +0 -10
  222. package/src/lib/hooks/hook-executor.js +0 -167
  223. package/src/lib/validators/checkpoint-persistence.js +0 -39
  224. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  225. package/src/lib/validators/shared/index.js +0 -12
  226. package/src/lib/validators/shared/issue-counter.js +0 -14
  227. 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" seria lido como "sem pricing" — e a ausencia silenciosa e o defeito.
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(); // idempotente
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
- // 1.000.000 de entrada = 1.25 USD; 100.000 de saida = 1.00 USD
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 de entrada, dos quais 800k vieram do cache:
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
- // sem tarifa de cache, a entrada inteira e cobrada como entrada — nunca de graca.
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
- // os quatro fatos juntos: Chat + tools + effort enviado + effort != none
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
- /// <summary>
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
- /// <summary>
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
- /// <summary>
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>();
@@ -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. O cacheado nao rende desconto.
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); // zero, jamais nulo
121
+ Assert.Equal(0m, record.CostUsd);
160
122
  Assert.Equal(UsageCostStatus.Waived, record.Status);
161
- Assert.False(record.IsMeasuredCost); // logo, nao entra na soma
123
+ Assert.False(record.IsMeasuredCost);
162
124
  Assert.Equal(alias.PricingWaiver, record.PricingWaiver);
163
- Assert.Equal(900, record.InputTokens); // os tokens continuam medidos
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()); // com dispensa escrita, passa
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 &gt; 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
- # Multi-stage Next.js production build for Coolify
2
- # Stage 1: Dependencies
3
- FROM node:20-alpine AS deps
4
- RUN apk add --no-cache libc6-compat
5
- WORKDIR /app
6
-
7
- COPY package.json package-lock.json* ./
8
- RUN npm ci --only=production && npm cache clean --force
9
-
10
- # Stage 2: Build
11
- FROM node:20-alpine AS builder
12
- WORKDIR /app
13
-
14
- COPY --from=deps /app/node_modules ./node_modules
15
- COPY . .
16
-
17
- # Build args for NEXT_PUBLIC_ variables (must be available at build time)
18
- ARG NEXT_PUBLIC_APP_URL
19
- ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
20
-
21
- RUN npm run build
22
-
23
- # Stage 3: Production runner
24
- FROM node:20-alpine AS runner
25
- WORKDIR /app
26
-
27
- ENV NODE_ENV=production
28
- ENV NEXT_TELEMETRY_DISABLED=1
29
-
30
- RUN addgroup --system --gid 1001 nodejs
31
- RUN adduser --system --uid 1001 nextjs
32
-
33
- COPY --from=builder /app/public ./public
34
- COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
35
- COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
36
-
37
- USER nextjs
38
-
39
- EXPOSE 3000
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"]