@polymorphism-tech/morph-spec 8.33.0 → 8.35.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 (253) hide show
  1. package/CLAUDE.md +22 -2
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +43 -5
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +5 -4
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +20 -22
  9. package/framework/agents.json +198 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +238 -10
  13. package/framework/evals/proof-rubrics/README.md +76 -0
  14. package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
  15. package/framework/evals/proof-rubrics/fiacao.md +54 -0
  16. package/framework/evals/proof-rubrics/guard-cego.md +52 -0
  17. package/framework/evals/proof-rubrics/media-claims.md +109 -0
  18. package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
  19. package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
  20. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
  21. package/framework/evals/rubrics/architecture.md +30 -0
  22. package/framework/evals/rubrics/code-quality.md +26 -0
  23. package/framework/evals/rubrics/test-coverage.md +20 -0
  24. package/framework/hooks/README.md +11 -1
  25. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  26. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  27. package/framework/hooks/claude-code/core/chain.js +27 -7
  28. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  29. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  30. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  32. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  33. package/framework/hooks/shared/command-tokens.js +23 -0
  34. package/framework/hooks/shared/e2e-check.js +0 -0
  35. package/framework/hooks/shared/git-paths.js +25 -0
  36. package/framework/hooks/shared/hook-response.js +17 -0
  37. package/framework/hooks/shared/morph-cli.js +134 -0
  38. package/framework/rules/ai-agents-standards.md +14 -2
  39. package/framework/rules/infrastructure-standards.md +3 -1
  40. package/framework/schemas/ai-pin.schema.json +97 -0
  41. package/framework/schemas/config.schema.json +107 -0
  42. package/framework/schemas/feature.schema.json +23 -4
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +28 -7
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  48. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  49. package/framework/skills/core/morph-implement/SKILL.md +20 -2
  50. package/framework/skills/core/morph-media/SKILL.md +215 -0
  51. package/framework/skills/core/morph-plan/SKILL.md +38 -4
  52. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  53. package/framework/skills/core/morph-review/SKILL.md +166 -9
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  56. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  57. package/framework/standards/PROVENANCE.json +81 -3
  58. package/framework/standards/STANDARDS.json +1504 -830
  59. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  60. package/framework/standards/ai-agents/agent-session.md +256 -160
  61. package/framework/standards/ai-agents/agent-spec.md +196 -0
  62. package/framework/standards/ai-agents/batching.md +217 -193
  63. package/framework/standards/ai-agents/context-providers.md +261 -188
  64. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  65. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  66. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  67. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  68. package/framework/standards/ai-agents/guardrails.md +222 -0
  69. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  70. package/framework/standards/ai-agents/mcp-server.md +351 -250
  71. package/framework/standards/ai-agents/mcp-tools.md +250 -197
  72. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  73. package/framework/standards/ai-agents/media-video.md +267 -0
  74. package/framework/standards/ai-agents/middleware-patterns.md +241 -207
  75. package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
  76. package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
  77. package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
  78. package/framework/standards/ai-agents/modalities-vision.md +205 -201
  79. package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
  80. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  81. package/framework/standards/ai-agents/production.md +260 -231
  82. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  83. package/framework/standards/ai-agents/providers/model-registry.md +293 -158
  84. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  85. package/framework/standards/ai-agents/service-tier-flex.md +143 -135
  86. package/framework/standards/ai-agents/setup.md +166 -50
  87. package/framework/standards/ai-agents/structured-output.md +280 -249
  88. package/framework/standards/ai-agents/sweet-spot.md +129 -109
  89. package/framework/standards/ai-agents/testing-ai.md +236 -0
  90. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  91. package/framework/standards/ai-agents/workflows.md +27 -7
  92. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  93. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  94. package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
  95. package/framework/templates/REGISTRY.json +14 -1
  96. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  97. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  98. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  99. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  100. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  101. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  102. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  103. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  203. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  204. package/package.json +3 -3
  205. package/src/commands/e2e/e2e.js +91 -40
  206. package/src/commands/project/doctor.js +143 -6
  207. package/src/commands/project/env.js +95 -0
  208. package/src/commands/project/update.js +469 -153
  209. package/src/commands/project/worktree.js +274 -53
  210. package/src/commands/state/advance.js +24 -2
  211. package/src/commands/state/approve.js +33 -1
  212. package/src/commands/state/create.js +72 -11
  213. package/src/commands/state/delete.js +8 -0
  214. package/src/commands/state/finish.js +360 -53
  215. package/src/commands/state/gate-check.js +190 -6
  216. package/src/commands/state/score.js +36 -1
  217. package/src/commands/validation/verify.js +134 -25
  218. package/src/core/state/state-manager.js +9 -1
  219. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  220. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  221. package/src/lib/ai/ai-checks.js +182 -0
  222. package/src/lib/ai/ai-project-scan.js +347 -0
  223. package/src/lib/ai/code-audit.js +181 -0
  224. package/src/lib/ai/comment-stripper.js +184 -0
  225. package/src/lib/ai/pin-drift.js +138 -0
  226. package/src/lib/ai/registry-audit.js +285 -0
  227. package/src/lib/e2e/compose-env.js +55 -14
  228. package/src/lib/e2e/stack-teardown.js +115 -0
  229. package/src/lib/evaluator-report.js +269 -15
  230. package/src/lib/generators/architecture-scanner.js +2 -1
  231. package/src/lib/git/default-branch.js +92 -4
  232. package/src/lib/graph/graphifyignore.js +2 -1
  233. package/src/lib/installers/effects.js +193 -0
  234. package/src/lib/installers/templates-reconcile.js +3 -3
  235. package/src/lib/risk-detector.js +284 -0
  236. package/src/lib/standards/verified-against.js +276 -0
  237. package/src/lib/tasks/eval-node.js +485 -0
  238. package/src/lib/tasks/linux-runner.js +297 -0
  239. package/src/lib/tasks/test-nodes.js +486 -0
  240. package/src/lib/tasks/test-runner.js +431 -37
  241. package/src/lib/validators/verify-runner.js +590 -28
  242. package/src/lib/worktree/dirs.js +32 -0
  243. package/src/lib/worktree/docker-audit.js +287 -0
  244. package/src/lib/worktree/env-allocator.js +69 -11
  245. package/src/lib/worktree/worktree-audit.js +70 -3
  246. package/src/scripts/setup-infra.js +24 -11
  247. package/src/utils/agents-installer.js +18 -8
  248. package/src/utils/claude-md-injector.js +10 -5
  249. package/src/utils/file-copier.js +28 -16
  250. package/src/utils/hooks-installer.js +75 -29
  251. package/src/utils/skills-installer.js +194 -187
  252. package/src/utils/version-checker.js +68 -3
  253. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -0,0 +1,225 @@
1
+ using System.Runtime.CompilerServices;
2
+ using Microsoft.Extensions.AI;
3
+
4
+ namespace Morph.AiKit.Tests.Fakes;
5
+
6
+ /// <summary>
7
+ /// <see cref="IChatClient"/> roteirizado: a costura que dispensa rede e chave de API na
8
+ /// suite inteira do kit.
9
+ /// </summary>
10
+ /// <remarks>
11
+ /// <para>
12
+ /// <b>Por que ele e o coracao da historia de teste:</b> o unico ponto em que o kit toca um
13
+ /// provider e um <see cref="IChatClient"/>. Trocando esse ponto por uma fila de respostas
14
+ /// escritas a mao, TODO o resto — <c>ModelRegistry</c>, <c>AgentFactory</c>,
15
+ /// <c>UsageCostMiddleware</c>, <c>ConversationStore</c> — passa a ser testavel sem socket,
16
+ /// sem segredo e sem flake. Nenhum tipo desta pasta abre cliente HTTP nem le variavel de
17
+ /// ambiente — o canario estrutural disso e um grep sobre <c>tests/</c> (chave de provider,
18
+ /// leitura de ambiente, cliente HTTP) que precisa voltar VAZIO, e o canario de
19
+ /// comportamento e
20
+ /// <c>FakeChatClientTests.TheWholeSeamAnswersFromMemoryAloneWithNoConfigurationAndNoConnection</c>.
21
+ /// </para>
22
+ /// <para>
23
+ /// <b>Chamada nao roteirizada e defeito de teste, nao "comportamento padrao".</b> Esgotada
24
+ /// a fila, o fake lanca com a contagem exata de chamadas. Devolver uma resposta vazia
25
+ /// silenciosamente faria um teste passar por engano — que e a unica coisa pior do que um
26
+ /// teste falhando.
27
+ /// </para>
28
+ /// <para>Seguro para uso concorrente: fila, contadores e capturas vivem sob um lock unico.</para>
29
+ /// </remarks>
30
+ public sealed class FakeChatClient : IChatClient
31
+ {
32
+ private readonly Lock _gate = new();
33
+ private readonly Queue<ChatResponse> _scripted = new();
34
+ private readonly List<ChatOptions?> _receivedOptions = [];
35
+ private readonly List<IReadOnlyList<ChatMessage>> _receivedMessages = [];
36
+
37
+ /// <summary>Cria um fake com a fila vazia. Enfileire antes de usar.</summary>
38
+ public FakeChatClient()
39
+ {
40
+ }
41
+
42
+ /// <summary>Cria um fake ja roteirizado com as respostas dadas, na ordem.</summary>
43
+ public FakeChatClient(params ChatResponse[] scripted)
44
+ {
45
+ ArgumentNullException.ThrowIfNull(scripted);
46
+
47
+ foreach (var response in scripted)
48
+ {
49
+ _scripted.Enqueue(response);
50
+ }
51
+ }
52
+
53
+ /// <summary>Atalho: uma resposta de assistente por texto, na ordem dada.</summary>
54
+ public static FakeChatClient Scripted(params string[] texts)
55
+ {
56
+ ArgumentNullException.ThrowIfNull(texts);
57
+
58
+ var client = new FakeChatClient();
59
+ foreach (var text in texts)
60
+ {
61
+ client.EnqueueText(text);
62
+ }
63
+
64
+ return client;
65
+ }
66
+
67
+ /// <summary>Quantas vezes o cliente foi invocado (streaming incluso).</summary>
68
+ public int CallCount { get; private set; }
69
+
70
+ /// <summary>
71
+ /// O <c>ProviderName</c> anunciado no <see cref="ChatClientMetadata"/>. Configuravel
72
+ /// porque ele ENTRA NA CHAVE da cache de eval: a arvore que este fake grava so e legivel
73
+ /// depois pelo <c>OfflineChatClient</c> se os dois se apresentarem com a mesma
74
+ /// identidade. Default preservado ("fake"): quem nao usa eval nao muda de comportamento.
75
+ /// </summary>
76
+ public string MetadataProviderName { get; init; } = "fake";
77
+
78
+ /// <summary>
79
+ /// O <c>DefaultModelId</c> anunciado no <see cref="ChatClientMetadata"/>. Tambem entra na
80
+ /// chave de cache. Nulo por default, como antes.
81
+ /// </summary>
82
+ public string? MetadataDefaultModelId { get; init; }
83
+
84
+ /// <summary>Se <see cref="Dispose"/> ja foi chamado — usado para provar ownership de cache.</summary>
85
+ public bool IsDisposed { get; private set; }
86
+
87
+ /// <summary>As <see cref="ChatOptions"/> recebidas, uma entrada por chamada, na ordem.</summary>
88
+ public IReadOnlyList<ChatOptions?> ReceivedOptions
89
+ {
90
+ get { lock (_gate) { return [.. _receivedOptions]; } }
91
+ }
92
+
93
+ /// <summary>As mensagens recebidas, uma entrada por chamada, na ordem.</summary>
94
+ public IReadOnlyList<IReadOnlyList<ChatMessage>> ReceivedMessages
95
+ {
96
+ get { lock (_gate) { return [.. _receivedMessages]; } }
97
+ }
98
+
99
+ /// <summary>As opcoes da ultima chamada, ou nulo se nunca foi chamado.</summary>
100
+ public ChatOptions? LastOptions
101
+ {
102
+ get { lock (_gate) { return _receivedOptions.Count == 0 ? null : _receivedOptions[^1]; } }
103
+ }
104
+
105
+ /// <summary>As mensagens da ultima chamada, ou nulo se nunca foi chamado.</summary>
106
+ public IReadOnlyList<ChatMessage>? LastMessages
107
+ {
108
+ get { lock (_gate) { return _receivedMessages.Count == 0 ? null : _receivedMessages[^1]; } }
109
+ }
110
+
111
+ /// <summary>Enfileira uma resposta ja montada. Devolve o proprio fake, para encadear.</summary>
112
+ public FakeChatClient Enqueue(ChatResponse response)
113
+ {
114
+ ArgumentNullException.ThrowIfNull(response);
115
+
116
+ lock (_gate)
117
+ {
118
+ _scripted.Enqueue(response);
119
+ }
120
+
121
+ return this;
122
+ }
123
+
124
+ /// <summary>
125
+ /// Enfileira uma resposta de assistente com o texto dado e, opcionalmente, um
126
+ /// <see cref="UsageDetails"/> controlado — e assim que o teste do
127
+ /// <c>UsageCostMiddleware</c> (T11) verifica tarifa sem rede.
128
+ /// </summary>
129
+ public FakeChatClient EnqueueText(string text, UsageDetails? usage = null)
130
+ => Enqueue(new ChatResponse(new ChatMessage(ChatRole.Assistant, text)) { Usage = usage });
131
+
132
+ /// <summary>
133
+ /// Enfileira uma resposta com contagem de tokens conhecida. Atalho para o caso mais
134
+ /// comum de teste de custo.
135
+ /// </summary>
136
+ public FakeChatClient EnqueueTextWithUsage(
137
+ string text,
138
+ long inputTokens,
139
+ long outputTokens,
140
+ long? cachedInputTokens = null)
141
+ => EnqueueText(
142
+ text,
143
+ new UsageDetails
144
+ {
145
+ InputTokenCount = inputTokens,
146
+ OutputTokenCount = outputTokens,
147
+ TotalTokenCount = inputTokens + outputTokens,
148
+ CachedInputTokenCount = cachedInputTokens,
149
+ });
150
+
151
+ /// <inheritdoc />
152
+ public Task<ChatResponse> GetResponseAsync(
153
+ IEnumerable<ChatMessage> messages,
154
+ ChatOptions? options = null,
155
+ CancellationToken cancellationToken = default)
156
+ {
157
+ ArgumentNullException.ThrowIfNull(messages);
158
+ cancellationToken.ThrowIfCancellationRequested();
159
+
160
+ return Task.FromResult(Record(messages, options));
161
+ }
162
+
163
+ /// <inheritdoc />
164
+ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
165
+ IEnumerable<ChatMessage> messages,
166
+ ChatOptions? options = null,
167
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
168
+ {
169
+ ArgumentNullException.ThrowIfNull(messages);
170
+ cancellationToken.ThrowIfCancellationRequested();
171
+
172
+ var response = Record(messages, options);
173
+
174
+ foreach (var update in response.ToChatResponseUpdates())
175
+ {
176
+ cancellationToken.ThrowIfCancellationRequested();
177
+ yield return update;
178
+ }
179
+
180
+ await Task.CompletedTask.ConfigureAwait(false);
181
+ }
182
+
183
+ /// <inheritdoc />
184
+ public object? GetService(Type serviceType, object? serviceKey = null)
185
+ {
186
+ ArgumentNullException.ThrowIfNull(serviceType);
187
+
188
+ if (serviceKey is not null)
189
+ {
190
+ return null;
191
+ }
192
+
193
+ if (serviceType == typeof(ChatClientMetadata))
194
+ {
195
+ return new ChatClientMetadata(MetadataProviderName, providerUri: null, MetadataDefaultModelId);
196
+ }
197
+
198
+ return serviceType.IsInstanceOfType(this) ? this : null;
199
+ }
200
+
201
+ /// <inheritdoc />
202
+ public void Dispose() => IsDisposed = true;
203
+
204
+ private ChatResponse Record(IEnumerable<ChatMessage> messages, ChatOptions? options)
205
+ {
206
+ var snapshot = messages as IReadOnlyList<ChatMessage> ?? [.. messages];
207
+
208
+ lock (_gate)
209
+ {
210
+ CallCount++;
211
+ _receivedMessages.Add(snapshot);
212
+ _receivedOptions.Add(options);
213
+
214
+ if (_scripted.Count == 0)
215
+ {
216
+ throw new InvalidOperationException(
217
+ $"FakeChatClient: fila de respostas esgotada na chamada #{CallCount}. " +
218
+ "Enfileire com Enqueue/EnqueueText antes de exercitar o caminho — uma " +
219
+ "resposta inventada aqui faria o teste passar por engano.");
220
+ }
221
+
222
+ return _scripted.Dequeue();
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,215 @@
1
+ using Microsoft.Extensions.AI;
2
+ using Morph.AiKit.Providers;
3
+ using Xunit;
4
+
5
+ namespace Morph.AiKit.Tests.Fakes;
6
+
7
+ /// <summary>
8
+ /// Canarios da costura de teste (T9). Um fake so vale se ele proprio for testado: um fake
9
+ /// que devolve na ordem errada, ou que engole as <see cref="ChatOptions"/> recebidas,
10
+ /// transforma todo teste que depende dele num teste que nao mede nada.
11
+ /// </summary>
12
+ /// <remarks>
13
+ /// Todo <c>await</c> passa <c>TestContext.Current.CancellationToken</c> porque o analisador
14
+ /// xUnit1051 cobra isso e <c>TreatWarningsAsErrors</c> transforma a cobranca em build
15
+ /// vermelho. Nao ha <c>#pragma</c> aqui: a regra e boa (teste cancelavel) e o custo e um
16
+ /// argumento.
17
+ /// </remarks>
18
+ public sealed class FakeChatClientTests
19
+ {
20
+ [Fact]
21
+ public async Task ResponsesComeBackInTheOrderTheyWereEnqueued()
22
+ {
23
+ using var client = FakeChatClient.Scripted("primeira", "segunda", "terceira");
24
+
25
+ var first = await client.GetResponseAsync(
26
+ [new ChatMessage(ChatRole.User, "1")],
27
+ cancellationToken: TestContext.Current.CancellationToken);
28
+ var second = await client.GetResponseAsync(
29
+ [new ChatMessage(ChatRole.User, "2")],
30
+ cancellationToken: TestContext.Current.CancellationToken);
31
+ var third = await client.GetResponseAsync(
32
+ [new ChatMessage(ChatRole.User, "3")],
33
+ cancellationToken: TestContext.Current.CancellationToken);
34
+
35
+ Assert.Equal(["primeira", "segunda", "terceira"], new[] { first.Text, second.Text, third.Text });
36
+ Assert.Equal(3, client.CallCount);
37
+ }
38
+
39
+ [Fact]
40
+ public async Task TheOptionsAndMessagesOfEveryCallAreExposedForInspection()
41
+ {
42
+ using var client = FakeChatClient.Scripted("ok", "ok");
43
+
44
+ var quente = new ChatOptions { Temperature = 0.9f };
45
+ var frio = new ChatOptions { Temperature = 0.1f, MaxOutputTokens = 64 };
46
+
47
+ await client.GetResponseAsync(
48
+ [new ChatMessage(ChatRole.User, "quente")],
49
+ quente,
50
+ TestContext.Current.CancellationToken);
51
+ await client.GetResponseAsync(
52
+ [new ChatMessage(ChatRole.User, "frio")],
53
+ frio,
54
+ TestContext.Current.CancellationToken);
55
+
56
+ Assert.Equal([quente, frio], client.ReceivedOptions);
57
+ Assert.Same(frio, client.LastOptions);
58
+
59
+ Assert.Equal(
60
+ ["quente", "frio"],
61
+ client.ReceivedMessages.Select(turn => turn[0].Text));
62
+ Assert.Equal("frio", client.LastMessages![0].Text);
63
+ }
64
+
65
+ [Fact]
66
+ public async Task OptionsAreCapturedAsNullWhenTheCallerSendsNone()
67
+ {
68
+ using var client = FakeChatClient.Scripted("ok");
69
+
70
+ await client.GetResponseAsync(
71
+ [new ChatMessage(ChatRole.User, "oi")],
72
+ cancellationToken: TestContext.Current.CancellationToken);
73
+
74
+ Assert.Null(Assert.Single(client.ReceivedOptions));
75
+ }
76
+
77
+ [Fact]
78
+ public async Task AnUnscriptedCallFailsLoudlyInsteadOfInventingAnAnswer()
79
+ {
80
+ using var client = FakeChatClient.Scripted("unica");
81
+
82
+ await client.GetResponseAsync(
83
+ [new ChatMessage(ChatRole.User, "1")],
84
+ cancellationToken: TestContext.Current.CancellationToken);
85
+
86
+ var error = await Assert.ThrowsAsync<InvalidOperationException>(
87
+ () => client.GetResponseAsync(
88
+ [new ChatMessage(ChatRole.User, "2")],
89
+ cancellationToken: TestContext.Current.CancellationToken));
90
+
91
+ Assert.Contains("esgotada na chamada #2", error.Message, StringComparison.Ordinal);
92
+ Assert.Equal(2, client.CallCount);
93
+ }
94
+
95
+ [Fact]
96
+ public async Task StreamingReplaysTheSameScriptedResponseAsUpdates()
97
+ {
98
+ using var client = FakeChatClient.Scripted("resposta em pedacos");
99
+
100
+ var text = string.Empty;
101
+ var stream = client.GetStreamingResponseAsync(
102
+ [new ChatMessage(ChatRole.User, "vai")],
103
+ cancellationToken: TestContext.Current.CancellationToken);
104
+
105
+ await foreach (var update in stream)
106
+ {
107
+ text += update.Text;
108
+ }
109
+
110
+ Assert.Equal("resposta em pedacos", text);
111
+ Assert.Equal(1, client.CallCount);
112
+ }
113
+
114
+ [Fact]
115
+ public async Task UsageIsControlledByTheTestAndSurvivesTheRoundTrip()
116
+ {
117
+ // E assim que a T11 vai verificar tarifa sem rede: Usage conhecido entra,
118
+ // Usage conhecido sai.
119
+ using var client = new FakeChatClient();
120
+ client.EnqueueTextWithUsage("ok", inputTokens: 1_000, outputTokens: 250, cachedInputTokens: 400);
121
+
122
+ var response = await client.GetResponseAsync(
123
+ [new ChatMessage(ChatRole.User, "oi")],
124
+ cancellationToken: TestContext.Current.CancellationToken);
125
+
126
+ Assert.NotNull(response.Usage);
127
+ Assert.Equal(1_000, response.Usage.InputTokenCount);
128
+ Assert.Equal(250, response.Usage.OutputTokenCount);
129
+ Assert.Equal(1_250, response.Usage.TotalTokenCount);
130
+ Assert.Equal(400, response.Usage.CachedInputTokenCount);
131
+ }
132
+
133
+ [Fact]
134
+ public void MetadataIsAnsweredThroughGetServiceLikeARealClient()
135
+ {
136
+ using var client = new FakeChatClient();
137
+
138
+ var metadata = Assert.IsType<ChatClientMetadata>(client.GetService(typeof(ChatClientMetadata)));
139
+ Assert.Equal("fake", metadata.ProviderName);
140
+
141
+ Assert.Same(client, client.GetService(typeof(IChatClient)));
142
+ Assert.Null(client.GetService(typeof(IChatClient), serviceKey: "qualquer"));
143
+ }
144
+
145
+ [Fact]
146
+ public void DisposeIsObservable()
147
+ {
148
+ var client = new FakeChatClient();
149
+
150
+ Assert.False(client.IsDisposed);
151
+ client.Dispose();
152
+ Assert.True(client.IsDisposed);
153
+ }
154
+
155
+ [Fact]
156
+ public async Task TheWholeSeamAnswersFromMemoryAloneWithNoConfigurationAndNoConnection()
157
+ {
158
+ // O caminho inteiro que os consumidores usam — documento JSON, registry, fabrica,
159
+ // cliente — exercitado com nada alem de objetos em memoria: nenhuma chave, nenhum
160
+ // arquivo, nenhum socket. E este o teste a apontar quando alguem perguntar "como o
161
+ // kit testa IA sem provider".
162
+ //
163
+ // A propriedade "passa sem chave de provider no ambiente" NAO e afirmada aqui por
164
+ // codigo: afirma-la exigiria ler variavel de ambiente, e um teste que faz isso
165
+ // derruba o canario estrutural da T9 — o grep sobre tests/ (chave de provider,
166
+ // leitura de ambiente, cliente HTTP) tem de voltar VAZIO. A propriedade e
167
+ // DEMONSTRADA rodando a suite com o ambiente esvaziado, o que cobre os 50 testes
168
+ // em vez de um.
169
+ var document = """
170
+ {
171
+ "version": "2.0",
172
+ "defaultAlias": "so-memoria",
173
+ "aliases": {
174
+ "so-memoria": {
175
+ "provider": "fake",
176
+ "model": "modelo-de-teste",
177
+ "pricingWaiver": "fake de teste: nao ha tarifa porque nao ha provider."
178
+ }
179
+ }
180
+ }
181
+ """;
182
+
183
+ var scripted = FakeChatClient.Scripted("resposta sem rede");
184
+ using var registry = ModelRegistry.FromJson(document, new FakeProviderClientFactory(_ => scripted));
185
+
186
+ var client = registry.GetChatClient("so-memoria");
187
+ var response = await client.GetResponseAsync(
188
+ [new ChatMessage(ChatRole.User, "oi")],
189
+ cancellationToken: TestContext.Current.CancellationToken);
190
+
191
+ Assert.Equal("resposta sem rede", response.Text);
192
+ Assert.Equal(1, scripted.CallCount);
193
+ }
194
+
195
+ [Fact]
196
+ public async Task ConcurrentCallersNeverLoseACallOrACapture()
197
+ {
198
+ // ConversationStore (T13) exercita duas sessoes sobre a MESMA instancia. Se o fake
199
+ // perdesse chamada sob concorrencia, aquele teste mediria o fake, nao o store.
200
+ using var client = new FakeChatClient();
201
+ for (var i = 0; i < 50; i++)
202
+ {
203
+ client.EnqueueText($"r{i}");
204
+ }
205
+
206
+ await Task.WhenAll(Enumerable.Range(0, 50).Select(i =>
207
+ client.GetResponseAsync(
208
+ [new ChatMessage(ChatRole.User, $"m{i}")],
209
+ cancellationToken: TestContext.Current.CancellationToken)));
210
+
211
+ Assert.Equal(50, client.CallCount);
212
+ Assert.Equal(50, client.ReceivedMessages.Count);
213
+ Assert.Equal(50, client.ReceivedOptions.Count);
214
+ }
215
+ }
@@ -0,0 +1,33 @@
1
+ namespace Morph.AiKit.Tests.Fakes;
2
+
3
+ /// <summary>
4
+ /// <see cref="TimeProvider"/> controlado pelo teste: o relogio so anda quando o teste manda.
5
+ /// </summary>
6
+ /// <remarks>
7
+ /// <para>
8
+ /// Existe <b>escrito a mao, em 20 linhas</b>, e nao via
9
+ /// <c>Microsoft.Extensions.TimeProvider.Testing</c>, por uma razao de contrato desta
10
+ /// feature: toda versao de pacote vem de <c>framework/ai-pin.json</c>, e aquele pacote nao
11
+ /// esta no pin. Acrescenta-lo seria uma versao decidida fora do pin — exatamente a segunda
12
+ /// verdade que o kit existe para impedir. O achado esta registrado no relatorio da T11.
13
+ /// </para>
14
+ /// <para>
15
+ /// So o necessario esta implementado: <see cref="GetUtcNow"/> e o avanco manual. Timers nao
16
+ /// sao usados por nenhum tipo do kit; herdar o comportamento base e mais honesto que
17
+ /// simular um agendador que ninguem exercita.
18
+ /// </para>
19
+ /// </remarks>
20
+ public sealed class FakeClock(DateTimeOffset start) : TimeProvider
21
+ {
22
+ private DateTimeOffset _now = start;
23
+
24
+ /// <inheritdoc />
25
+ public override DateTimeOffset GetUtcNow() => _now;
26
+
27
+ /// <summary>Avanca o relogio. Rejeita andar para tras: o tempo do teste e monotono.</summary>
28
+ public void Advance(TimeSpan delta)
29
+ {
30
+ ArgumentOutOfRangeException.ThrowIfLessThan(delta, TimeSpan.Zero);
31
+ _now += delta;
32
+ }
33
+ }
@@ -0,0 +1,89 @@
1
+ using Morph.AiKit.Media;
2
+
3
+ namespace Morph.AiKit.Tests.Fakes;
4
+
5
+ /// <summary>
6
+ /// <see cref="IMediaProvider"/> roteirizado: a costura que dispensa rede, chave de API e
7
+ /// <b>fatura</b> na suite inteira de midia.
8
+ /// </summary>
9
+ /// <remarks>
10
+ /// <para>
11
+ /// <b>Por que ele existe:</b> o unico ponto do subsistema que gasta dinheiro e um
12
+ /// <c>IMediaProvider</c>. Trocando-o por uma fila de respostas escritas a mao, todo o resto
13
+ /// — <c>MediaGenerator</c>, <c>MediaWorker</c>, <c>MediaJob</c>, <c>MediaStore</c> — passa a
14
+ /// ser testavel sem socket, sem segredo e sem cobranca. Mesmo desenho do
15
+ /// <see cref="FakeChatClient"/>, pela mesma razao.
16
+ /// </para>
17
+ /// <para>
18
+ /// Ele so e construivel porque <c>InternalsVisibleTo("Morph.AiKit.Tests")</c> existe no
19
+ /// <c>.csproj</c> da biblioteca. Um projeto consumidor <b>nao</b> consegue escrever esta
20
+ /// classe — e essa impossibilidade e a regra dura de <c>ai-agents/media-pipeline.md</c>,
21
+ /// asserida em <c>MediaAccessibilityTests</c>.
22
+ /// </para>
23
+ /// <para>
24
+ /// <b>Chamada nao roteirizada e defeito de teste.</b> Esgotada a fila, o fake devolve uma
25
+ /// falha nomeada com a contagem de chamadas, em vez de inventar bytes — um artefato
26
+ /// fabricado pelo proprio fake faria um teste passar por engano.
27
+ /// </para>
28
+ /// </remarks>
29
+ internal sealed class FakeMediaProvider : IMediaProvider
30
+ {
31
+ private readonly Queue<MediaResult<MediaArtifact>> _scripted = new();
32
+ private readonly Lock _gate = new();
33
+
34
+ public FakeMediaProvider(string provider = "fake", MediaKind supports = MediaKind.Image)
35
+ {
36
+ Provider = provider;
37
+ Supported = supports;
38
+ }
39
+
40
+ /// <inheritdoc />
41
+ public string Provider { get; }
42
+
43
+ /// <summary>Que tipo de midia este fake diz atender.</summary>
44
+ public MediaKind Supported { get; }
45
+
46
+ /// <summary>Quantas vezes foi chamado. E como um teste prova que NAO houve segunda chamada paga.</summary>
47
+ public int Calls { get; private set; }
48
+
49
+ /// <summary>Os pedidos vistos, na ordem.</summary>
50
+ public List<MediaRequest> Seen { get; } = [];
51
+
52
+ /// <summary>Enfileira bytes de sucesso.</summary>
53
+ public FakeMediaProvider Returns(byte[] bytes, string mediaType = "image/png", string? revisedPrompt = null)
54
+ {
55
+ _scripted.Enqueue(MediaResult<MediaArtifact>.Ok(new MediaArtifact(
56
+ Supported, mediaType, bytes, revisedPrompt, ResponseId: "resp-fake", C2paPreserved: true)));
57
+ return this;
58
+ }
59
+
60
+ /// <summary>Enfileira uma recusa com o codigo dado — inclusive <c>PolicyBlocked</c>.</summary>
61
+ public FakeMediaProvider Fails(MediaErrorCode code, string message = "roteirizado")
62
+ {
63
+ _scripted.Enqueue(MediaResult<MediaArtifact>.Fail(code, message));
64
+ return this;
65
+ }
66
+
67
+ /// <inheritdoc />
68
+ public bool Supports(MediaKind kind) => kind == Supported;
69
+
70
+ /// <inheritdoc />
71
+ public Task<MediaResult<MediaArtifact>> ProduceAsync(
72
+ MediaRequest request,
73
+ MediaAliasBinding binding,
74
+ CancellationToken cancellationToken)
75
+ {
76
+ lock (_gate)
77
+ {
78
+ Calls++;
79
+ Seen.Add(request);
80
+
81
+ return Task.FromResult(_scripted.Count > 0
82
+ ? _scripted.Dequeue()
83
+ : MediaResult<MediaArtifact>.Fail(
84
+ MediaErrorCode.ProviderFailure,
85
+ $"FakeMediaProvider: chamada {Calls} nao roteirizada. Um fake que inventa bytes faz " +
86
+ "um teste passar por engano."));
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,55 @@
1
+ using Microsoft.Extensions.AI;
2
+ using Morph.AiKit.Providers;
3
+
4
+ namespace Morph.AiKit.Tests.Fakes;
5
+
6
+ /// <summary>
7
+ /// <see cref="IProviderClientFactory"/> de teste: devolve <see cref="FakeChatClient"/> e
8
+ /// <b>conta quantas vezes foi chamada</b>. E o contador que torna a memoizacao do
9
+ /// <c>ModelRegistry</c> uma afirmacao verificavel em vez de uma promessa.
10
+ /// </summary>
11
+ public sealed class FakeProviderClientFactory : IProviderClientFactory
12
+ {
13
+ private readonly Func<ModelAlias, IChatClient?> _create;
14
+ private readonly Dictionary<string, IChatClient> _created = new(StringComparer.Ordinal);
15
+ private readonly List<ModelAlias> _requested = [];
16
+
17
+ /// <summary>Fabrica que devolve um <see cref="FakeChatClient"/> novo por alias.</summary>
18
+ public FakeProviderClientFactory()
19
+ : this(_ => new FakeChatClient())
20
+ {
21
+ }
22
+
23
+ /// <summary>
24
+ /// Fabrica com construcao sob medida — inclusive devolvendo nulo, para exercitar a
25
+ /// recusa do registry a um cliente inexistente.
26
+ /// </summary>
27
+ public FakeProviderClientFactory(Func<ModelAlias, IChatClient?> create)
28
+ => _create = create;
29
+
30
+ /// <summary>Quantas vezes <see cref="CreateChatClient"/> foi realmente executada.</summary>
31
+ public int CreateCount { get; private set; }
32
+
33
+ /// <summary>Os aliases para os quais um cliente foi criado, na ordem.</summary>
34
+ public IReadOnlyList<ModelAlias> Requested => _requested;
35
+
36
+ /// <summary>Os clientes criados, por nome de alias.</summary>
37
+ public IReadOnlyDictionary<string, IChatClient> Created => _created;
38
+
39
+ /// <inheritdoc />
40
+ public IChatClient CreateChatClient(ModelAlias alias)
41
+ {
42
+ ArgumentNullException.ThrowIfNull(alias);
43
+
44
+ CreateCount++;
45
+ _requested.Add(alias);
46
+
47
+ var client = _create(alias);
48
+ if (client is not null)
49
+ {
50
+ _created[alias.Name] = client;
51
+ }
52
+
53
+ return client!;
54
+ }
55
+ }
@@ -0,0 +1,60 @@
1
+ using Morph.AiKit.Middleware;
2
+
3
+ namespace Morph.AiKit.Tests.Fakes;
4
+
5
+ /// <summary>
6
+ /// Sink de teste: acumula os registros de custo publicados pelo
7
+ /// <see cref="UsageCostMiddleware"/>.
8
+ /// </summary>
9
+ /// <remarks>
10
+ /// <para>
11
+ /// Estava nos <c>outputs</c> da T9, mas depende de <see cref="IUsageCostSink"/> e
12
+ /// <see cref="UsageCostRecord"/>, que sao <c>outputs</c> da T11. A T9 recusou-se a criar os
13
+ /// dois tipos por fora (seriam arquivos de outra task, e duplica-los criaria a segunda
14
+ /// verdade do registro de custo) e deixou este codigo pronto no seu relatorio; a errata E8
15
+ /// do <c>spec.md</c> moveu formalmente o arquivo para ca. Nasce junto dos tipos que
16
+ /// implementa.
17
+ /// </para>
18
+ /// <para>
19
+ /// Seguro sob concorrencia pelo mesmo motivo do <see cref="FakeChatClient"/>: um mesmo
20
+ /// pipeline atende varias conversas, e um sink que perdesse linha sob concorrencia faria o
21
+ /// teste medir o sink em vez do middleware.
22
+ /// </para>
23
+ /// </remarks>
24
+ public sealed class FakeUsageCostSink : IUsageCostSink
25
+ {
26
+ private readonly Lock _gate = new();
27
+ private readonly List<UsageCostRecord> _records = [];
28
+
29
+ /// <summary>Tudo o que foi publicado, na ordem. Snapshot — iterar e seguro.</summary>
30
+ public IReadOnlyList<UsageCostRecord> Records
31
+ {
32
+ get { lock (_gate) { return [.. _records]; } }
33
+ }
34
+
35
+ /// <summary>O ultimo registro publicado, ou nulo se nenhum foi.</summary>
36
+ public UsageCostRecord? Last
37
+ {
38
+ get { lock (_gate) { return _records.Count == 0 ? null : _records[^1]; } }
39
+ }
40
+
41
+ /// <summary>Quantas linhas de custo foram publicadas.</summary>
42
+ public int Count
43
+ {
44
+ get { lock (_gate) { return _records.Count; } }
45
+ }
46
+
47
+ /// <summary>A soma dos custos <b>medidos</b> — os demais status nao entram na conta.</summary>
48
+ public decimal MeasuredTotalUsd
49
+ {
50
+ get { lock (_gate) { return _records.Where(r => r.IsMeasuredCost).Sum(r => r.CostUsd); } }
51
+ }
52
+
53
+ /// <inheritdoc />
54
+ public void Record(UsageCostRecord record)
55
+ {
56
+ ArgumentNullException.ThrowIfNull(record);
57
+
58
+ lock (_gate) { _records.Add(record); }
59
+ }
60
+ }