@polymorphism-tech/morph-spec 8.38.0 → 8.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CLAUDE.md +20 -9
  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 +4 -3
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +43 -26
  10. package/framework/docs/CLI.md +7 -5
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/hooks/README.md +1 -1
  14. package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
  15. package/framework/hooks/claude-code/core/worktree-create.js +22 -6
  16. package/framework/hooks/shared/git-paths.js +38 -5
  17. package/framework/mcp/registry.json +58 -66
  18. package/framework/rules/infrastructure-standards.md +0 -1
  19. package/framework/rules/morph-workflow.md +9 -4
  20. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  21. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  22. package/framework/skills/core/morph-apply/SKILL.md +59 -25
  23. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  24. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  25. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  26. package/framework/skills/core/morph-implement/SKILL.md +48 -24
  27. package/framework/skills/core/morph-media/SKILL.md +2 -2
  28. package/framework/skills/core/morph-plan/SKILL.md +10 -3
  29. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  30. package/framework/skills/core/morph-review/SKILL.md +1 -1
  31. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  32. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  33. package/framework/skills/core/morph-uiux/SKILL.md +7 -6
  34. package/framework/standards/PROVENANCE.json +4 -4
  35. package/framework/standards/STANDARDS.json +9 -9
  36. package/framework/standards/ai-agents/context-providers.md +16 -26
  37. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  38. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  39. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  40. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  41. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  42. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  43. package/framework/standards/architecture/code-comments.md +52 -16
  44. package/framework/standards/backend/dotnet/testing.md +4 -2
  45. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  46. package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
  47. package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
  48. package/framework/standards/frontend/nextjs/testing.md +4 -2
  49. package/framework/standards/integration/mcp/mcp-tools.md +64 -126
  50. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  51. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  52. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  53. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  54. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  55. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  56. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  57. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  58. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  59. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  60. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  61. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  62. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  63. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  64. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  65. package/framework/templates/code/dotnet/test.cs +2 -87
  66. package/framework/templates/code/typescript/contracts.ts +140 -167
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  142. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  143. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  144. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  145. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  146. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  147. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  150. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  151. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  152. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  153. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  154. package/framework/templates/integrations/asaas-client.cs +332 -387
  155. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  156. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  157. package/framework/templates/project/validate.js +97 -120
  158. package/package.json +2 -2
  159. package/src/commands/dashboard/dashboard.js +80 -53
  160. package/src/commands/mcp/mcp-setup.js +37 -9
  161. package/src/commands/project/doctor.js +12 -6
  162. package/src/commands/project/init.js +6 -3
  163. package/src/commands/project/install-plugin-cmd.js +1 -1
  164. package/src/commands/project/update.js +5 -0
  165. package/src/commands/project/worktree.js +60 -4
  166. package/src/commands/state/gate-check.js +5 -0
  167. package/src/commands/tasks/dag.js +27 -4
  168. package/src/commands/validation/verify.js +22 -38
  169. package/src/dashboard/app/App.jsx +80 -16
  170. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  171. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  172. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  173. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  174. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  175. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  176. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  177. package/src/dashboard/app/components/Markdown.jsx +47 -0
  178. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  179. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  180. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  181. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  182. package/src/dashboard/app/hooks/usePref.js +38 -0
  183. package/src/dashboard/app/lib/format.js +69 -0
  184. package/src/dashboard/app/lib/phases.js +56 -0
  185. package/src/dashboard/app/lib/project.js +50 -0
  186. package/src/dashboard/app/pages/Activity.jsx +24 -0
  187. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  188. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  189. package/src/dashboard/app/pages/Features.jsx +400 -0
  190. package/src/dashboard/app/pages/Guide.jsx +146 -0
  191. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  192. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  193. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  194. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  195. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  196. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  197. package/src/dashboard/app/pages/Standards.jsx +407 -332
  198. package/src/dashboard/app/styles/index.css +187 -47
  199. package/src/dashboard/index.html +14 -12
  200. package/src/dashboard/package.json +32 -24
  201. package/src/dashboard/scanner/activity-scanner.js +351 -0
  202. package/src/dashboard/scanner/agents-scanner.js +59 -1
  203. package/src/dashboard/scanner/docs-scanner.js +59 -0
  204. package/src/dashboard/scanner/feature-status.js +10 -0
  205. package/src/dashboard/scanner/features-scanner.js +360 -0
  206. package/src/dashboard/scanner/index.js +14 -7
  207. package/src/dashboard/scanner/phases-scanner.js +14 -15
  208. package/src/dashboard/scanner/portfolio.js +153 -0
  209. package/src/dashboard/scanner/project-api.js +195 -0
  210. package/src/dashboard/scanner/rules-scanner.js +42 -37
  211. package/src/dashboard/scanner/skills-scanner.js +105 -43
  212. package/src/dashboard/scanner/standards-scanner.js +2 -0
  213. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  214. package/src/lib/detectors/claude-config-detector.js +2 -7
  215. package/src/lib/detectors/index.js +5 -1
  216. package/src/lib/evaluator-report.js +22 -0
  217. package/src/lib/generators/settings-generator.js +208 -209
  218. package/src/lib/installers/mcp-installer.js +724 -719
  219. package/src/lib/installers/memory-installer.js +1 -2
  220. package/src/lib/projects/registry.js +154 -0
  221. package/src/lib/risk-detector.js +13 -0
  222. package/src/lib/tasks/tasks-json-parser.js +21 -0
  223. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  224. package/src/lib/worktree/env-allocator.js +9 -3
  225. package/src/scripts/install-plugin.js +1 -1
  226. package/src/scripts/setup-infra.js +2 -4
  227. package/src/utils/agents-installer.js +108 -13
  228. package/src/utils/banner.js +1 -1
  229. package/src/commands/index.js +0 -13
  230. package/src/commands/project/checkpoint.js +0 -209
  231. package/src/core/state/phase-state-machine.js +0 -197
  232. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  233. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  234. package/src/dashboard/scanner/squads-scanner.js +0 -13
  235. package/src/dashboard/scanner/state-scanner.js +0 -85
  236. package/src/lib/generators/index.js +0 -10
  237. package/src/lib/hooks/hook-executor.js +0 -167
  238. package/src/lib/validators/checkpoint-persistence.js +0 -39
  239. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  240. package/src/lib/validators/shared/index.js +0 -12
  241. package/src/lib/validators/shared/issue-counter.js +0 -14
  242. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -5,17 +5,6 @@ using Xunit;
5
5
 
6
6
  namespace Morph.AiKit.Tests.Evals;
7
7
 
8
- /// <summary>
9
- /// A prova que decide a feature: <b>com a cache quente, o eval roda sem credencial e sem
10
- /// rede</b>.
11
- /// </summary>
12
- /// <remarks>
13
- /// Essa frase foi derivada de leitura de fonte antes de existir codigo. Aqui ela vira
14
- /// medicao: uma arvore de cache e gravada por um fake, o processo troca o cliente interno
15
- /// por um que RECUSA qualquer chamada, e a suite termina verde. Se o cliente interno fosse
16
- /// tocado uma unica vez, o contador dele deixaria de ser zero — e nenhuma das assercoes
17
- /// abaixo passaria.
18
- /// </remarks>
19
8
  public sealed class EvalFixtureTests
20
9
  {
21
10
  [Fact]
@@ -25,18 +14,12 @@ public sealed class EvalFixtureTests
25
14
  ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
26
15
  ws.WriteScenario("triagem", "acme", "handoff", "quero falar com humano", """[ { "kind": "contains", "value": "humano" } ]""");
27
16
 
28
- // 1) GRAVA a arvore contra o fake — nunca contra uma API paga. O caminho de escrita
29
- // exercitado e o da biblioteca, e a chave e computada pela mesma funcao que a
30
- // leitura vai usar.
31
17
  using var fake = new FakeChatClient
32
18
  {
33
19
  MetadataProviderName = "morph-fake",
34
20
  MetadataDefaultModelId = "modelo-exemplar",
35
21
  };
36
- // A ORDEM importa e e a do dataset (ordinal por caminho): "handoff" vem antes de
37
- // "saudacao". Enfileirar na ordem alfabetica errada foi como esta suite descobriu
38
- // que LoadAll ordena — e a ordem estavel e o que faz duas maquinas gravarem a mesma
39
- // arvore de cache.
22
+ // A ordem e a do dataset (ordinal por caminho): "handoff" antes de "saudacao".
40
23
  fake.EnqueueText("Encaminhando para um humano agora.").EnqueueText("Ola! Bom dia.");
41
24
 
42
25
  await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
@@ -50,8 +33,6 @@ public sealed class EvalFixtureTests
50
33
  Assert.Equal(2, fake.CallCount);
51
34
  Assert.True(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)), "o refresh tem de deixar o carimbo na raiz da cache");
52
35
 
53
- // 2) LE offline. O cliente interno agora RECUSA: se a cache nao respondesse, este
54
- // bloco lancaria EvalCacheMissException em vez de terminar verde.
55
36
  await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
56
37
  var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
57
38
 
@@ -63,8 +44,6 @@ public sealed class EvalFixtureTests
63
44
  Assert.Equal("ok", summary.Stamp);
64
45
  Assert.Equal(0, offline.Offline!.Invocations);
65
46
 
66
- // 3) O fake nao foi tocado de novo: a leitura offline nao passou por ele nem por
67
- // acidente (ele esta noutro processo logico, mas a contagem prova a intencao).
68
47
  Assert.Equal(2, fake.CallCount);
69
48
  }
70
49
 
@@ -84,8 +63,6 @@ public sealed class EvalFixtureTests
84
63
  await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
85
64
  await offline.RunAllAsync(TestContext.Current.CancellationToken);
86
65
 
87
- // Ler de volta o ARQUIVO, e nao o objeto em memoria: o contrato com o CLI e o
88
- // arquivo, e um teste que so olhasse o objeto seria um espelho do gerador.
89
66
  var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
90
67
  Assert.NotNull(reread);
91
68
  Assert.Equal(1, reread!.Scenarios);
@@ -103,17 +80,12 @@ public sealed class EvalFixtureTests
103
80
  var error = Assert.Throws<ArgumentOutOfRangeException>(() => EvalFixture.Create(options));
104
81
 
105
82
  Assert.Contains("eval-cache-ttl-too-short", error.Message, StringComparison.Ordinal);
106
- // A mensagem tem de dizer POR QUE, e nao so que o numero e pequeno: a expiracao apaga
107
- // o diretorio da chave durante a LEITURA.
108
83
  Assert.Contains("DESTRUTIVA NA LEITURA", error.Message, StringComparison.Ordinal);
109
84
  }
110
85
 
111
86
  [Fact]
112
87
  public void TheLibraryDefaultTimeToLiveWouldExpireACommittedCacheWithinWeeks()
113
88
  {
114
- // MEDIDO, nao lembrado: e este fato que torna o TTL explicito obrigatorio. Se um dia
115
- // a biblioteca subir o default para alem do piso do kit, este teste morre e a
116
- // exigencia pode ser rediscutida com um numero na mao.
117
89
  Assert.True(
118
90
  Defaults.DefaultTimeToLiveForCacheEntries < EvalCacheOptions.MinimumTimeToLive,
119
91
  $"o default da biblioteca e {Defaults.DefaultTimeToLiveForCacheEntries.TotalDays:0} dias, "
@@ -6,20 +6,9 @@ using Xunit;
6
6
 
7
7
  namespace Morph.AiKit.Tests.Evals;
8
8
 
9
- /// <summary>
10
- /// Regravacao, e o invariante barato que protege a premissa de commitabilidade:
11
- /// <b>uma leitura nao escreve um byte na arvore de cache</b>.
12
- /// </summary>
13
9
  public sealed class EvalRefreshTests
14
10
  {
15
- /// <summary>
16
- /// Impressao digital da arvore: caminho relativo mais SHA-256 de cada arquivo.
17
- /// </summary>
18
- /// <remarks>
19
- /// Comparar CONTEUDO, e nao data de modificacao: o que importa e se o proximo
20
- /// <c>git status</c> mostra diff, e o git tambem olha conteudo. Um teste sobre mtime
21
- /// falharia por gravacao identica e passaria por gravacao diferente no mesmo tick.
22
- /// </remarks>
11
+ // Compara conteudo, nao mtime: e o conteudo que o git ve como diff.
23
12
  private static IReadOnlyList<string> Fingerprint(string root)
24
13
  {
25
14
  if (!Directory.Exists(root))
@@ -63,9 +52,6 @@ public sealed class EvalRefreshTests
63
52
  Assert.Equal(3, summary.Passed);
64
53
  }
65
54
 
66
- // Se algum dia a biblioteca passar a tocar disco no caminho de HIT, este canario morde
67
- // no dia em que isso acontecer — e nao meses depois, quando alguem estranhar um diff
68
- // que aparece sozinho em toda rodada.
69
55
  Assert.Equal(before, Fingerprint(ws.CacheRoot));
70
56
  }
71
57
 
@@ -79,9 +65,6 @@ public sealed class EvalRefreshTests
79
65
  await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
80
66
  await refresh.RunAllAsync(TestContext.Current.CancellationToken);
81
67
 
82
- // Os resultados carregam latencia de cronometro, uso de tokens e o flag de cache hit:
83
- // commita-los seria diff garantido em toda rodada, sem sinal nenhum. Por CONSTRUCAO
84
- // (outra raiz), nunca por um .gitignore dentro de um diretorio commitado.
85
68
  Assert.True(Directory.Exists(ws.ResultsRoot), "o store de resultados tem de existir depois de um run");
86
69
  Assert.NotEmpty(Directory.EnumerateFiles(ws.ResultsRoot, "*", SearchOption.AllDirectories));
87
70
 
@@ -102,8 +85,6 @@ public sealed class EvalRefreshTests
102
85
  await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
103
86
  var summary = await refresh.RunAllAsync(TestContext.Current.CancellationToken);
104
87
 
105
- // O contador do cliente real e o que torna "a cache esta fria" uma MEDICAO: a arvore
106
- // mudou e precisa ser commitada, e isso e afirmacao com consequencia.
107
88
  Assert.Equal(3, summary.Misses);
108
89
  Assert.Equal(EvalCacheState.Cold, summary.Cache);
109
90
  Assert.Equal(3, fake.CallCount);
@@ -121,9 +102,6 @@ public sealed class EvalRefreshTests
121
102
  await first.RunAllAsync(TestContext.Current.CancellationToken);
122
103
  }
123
104
 
124
- // O cenario do meio some do dataset. Uma regravacao PARCIAL deixaria a entrada antiga
125
- // dele viva na arvore, e a proxima leitura offline nao teria como saber que ela nao
126
- // corresponde mais a nenhum cenario declarado.
127
105
  File.Delete(Path.Combine(ws.DatasetRoot, "triagem", "acme", "b-segundo.json"));
128
106
 
129
107
  using (var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" })
@@ -144,24 +122,14 @@ public sealed class EvalRefreshTests
144
122
  using var ws = new EvalWorkspace();
145
123
  using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
146
124
 
147
- // A lista vem do KIT, e nao do teste: e ela que da ao pacote .Quality cobertura de
148
- // COMPILACAO em src/ (sem um using real la, o pin daquele pacote teria so contrato de
149
- // versao, e um rename de API nao reprovaria PR nenhuma).
150
125
  var quality = EvalQualityEvaluators.Stable();
151
126
 
152
127
  await using var fixture = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake, quality);
153
128
 
154
- // A superficie existe e COMPILA contra as versoes do pin — que e o que um kit de
155
- // template tem de garantir. O que ela NAO faz e decidir: o veredito de um cenario sai
156
- // das assercoes deterministicas, e uma metrica de juizo entra no sumario como numero.
157
- // Sem isso, a variancia de LLM reprovaria PRs e o no de eval nasceria destinado a ser
158
- // desligado do CI.
159
129
  Assert.Equal(2, quality.Count);
160
130
  Assert.All(quality, evaluator => Assert.NotEmpty(evaluator.EvaluationMetricNames));
161
131
  Assert.Empty(fixture.Summary.Quality);
162
132
 
163
- // Uma lista NOVA a cada chamada: um array compartilhado seria estado global
164
- // disfarcado de conveniencia, e a ReportingConfiguration guarda o que recebe.
165
133
  Assert.NotSame(quality, EvalQualityEvaluators.Stable());
166
134
  }
167
135
 
@@ -172,11 +140,7 @@ public sealed class EvalRefreshTests
172
140
  [InlineData("Microsoft.Extensions.AI.Evaluation.Quality.ToolCallAccuracyEvaluator", true)]
173
141
  public void WhichQualityEvaluatorsAreExperimentalIsMEASURED_NotRemembered(string typeName, bool experimental)
174
142
  {
175
- // O plano descrevia os QUATRO como estaveis. Dois nao sao: eles carregam
176
- // [Experimental("AIEVAL001")] na 10.9.0, e usa-los sob TreatWarningsAsErrors exigiria
177
- // suprimir um diagnostico — uma decisao consciente, nunca um efeito colateral. Se um
178
- // dia a Microsoft estabilizar (ou marcar) algum deles, este teste morre e a escolha e
179
- // rediscutida com o fato na mao, em vez de envelhecer em silencio num comentario.
143
+ // Dois dos quatro avaliadores sao [Experimental("AIEVAL001")] na 10.9.0 e exigiriam supressao sob TreatWarningsAsErrors.
180
144
  var type = typeof(RelevanceEvaluator).Assembly.GetType(typeName);
181
145
  Assert.NotNull(type);
182
146
 
@@ -2,15 +2,6 @@ using Morph.AiKit.Evals;
2
2
 
3
3
  namespace Morph.AiKit.Tests.Evals;
4
4
 
5
- /// <summary>
6
- /// Um diretorio temporario com dataset, cache e resultados — a montagem que todo teste de
7
- /// eval precisa, escrita uma vez.
8
- /// </summary>
9
- /// <remarks>
10
- /// Ele existe para que os testes falem de COMPORTAMENTO e nao de caminhos. E cada instancia
11
- /// tem raiz propria (GUID v7): dois testes rodando lado a lado nao podem disputar a mesma
12
- /// arvore de cache, senao a falha aparece como flake e nao como defeito.
13
- /// </remarks>
14
5
  internal sealed class EvalWorkspace : IDisposable
15
6
  {
16
7
  public EvalWorkspace()
@@ -30,7 +21,6 @@ internal sealed class EvalWorkspace : IDisposable
30
21
 
31
22
  public string SummaryPath => Path.Combine(Root, "summary.json");
32
23
 
33
- /// <summary>O pin exemplar: a data e as versoes sob as quais a arvore diz ter sido gravada.</summary>
34
24
  public static EvalPin Pin { get; } = new(
35
25
  "2026-09-08",
36
26
  new Dictionary<string, string>(StringComparer.Ordinal)
@@ -39,7 +29,6 @@ internal sealed class EvalWorkspace : IDisposable
39
29
  ["Microsoft.Extensions.AI.Evaluation"] = "10.9.0",
40
30
  });
41
31
 
42
- /// <summary>Escreve um cenario no dataset e devolve o caminho do arquivo.</summary>
43
32
  public string WriteScenario(string agent, string tenant, string name, string userText, string assertions)
44
33
  {
45
34
  var dir = Path.Combine(DatasetRoot, agent, tenant);
@@ -54,7 +43,6 @@ internal sealed class EvalWorkspace : IDisposable
54
43
  return file;
55
44
  }
56
45
 
57
- /// <summary>Opcoes prontas para um modo, com TTL longo e o pin exemplar.</summary>
58
46
  public EvalCacheOptions Options(EvalMode mode, EvalPin? pin = null) => new()
59
47
  {
60
48
  CacheRoot = CacheRoot,
@@ -79,8 +67,7 @@ internal sealed class EvalWorkspace : IDisposable
79
67
  }
80
68
  catch (IOException)
81
69
  {
82
- // Um temporario que resiste a remocao nao pode derrubar um teste que ja provou o
83
- // que veio provar.
70
+ // Falha ao apagar o temporario nao deve derrubar um teste que ja provou o que veio provar.
84
71
  }
85
72
  }
86
73
  }
@@ -4,42 +4,14 @@ using Xunit;
4
4
 
5
5
  namespace Morph.AiKit.Tests.Evals;
6
6
 
7
- /// <summary>
8
- /// Os evals DO PROPRIO KIT, contra a arvore de cache COMMITADA — sem credencial, sem rede.
9
- /// </summary>
10
- /// <remarks>
11
- /// <para>
12
- /// Esta classe e o exemplar que um projeto copia. Ela roda com a categoria <c>Eval</c>, le a
13
- /// cache commitada em <c>eval-cache/</c> e o dataset versionado em <c>eval-datasets/</c>, e
14
- /// termina verde com TODAS as variaveis de credencial ausentes. Se algum cenario nao estiver
15
- /// na cache, ela fica VERMELHA nomeando o cenario — nunca <c>skip</c>.
16
- /// </para>
17
- /// <para>
18
- /// <b>A cache foi semeada por FAKE, jamais por chamada paga</b> (ver <see cref="SeedTheCommittedCacheTree"/>).
19
- /// O caminho de ESCRITA exercitado e o da biblioteca e a chave e computada pela mesma funcao
20
- /// que a leitura usa — entao a leitura offline le exatamente o que aquela escrita produziu.
21
- /// A lacuna residual, declarada em voz alta: isto NAO prova que uma cache gravada contra um
22
- /// provider real e legivel offline. O que a mitiga e um fato medido — a chave depende de
23
- /// <c>ProviderName</c> e <c>DefaultModelId</c>, e nao do endpoint, e os dois estao sob nosso
24
- /// controle no cliente offline.
25
- /// </para>
26
- /// </remarks>
27
7
  public sealed class KitEvals
28
8
  {
29
- /// <summary>Identidade sob a qual a arvore commitada foi gravada. Ela ENTRA NA CHAVE.</summary>
9
+ // Provider e Model entram na chave de cache: trocar qualquer um invalida a arvore commitada.
30
10
  private const string Provider = "morph-fake";
31
11
 
32
- /// <summary>Idem. Trocar qualquer um dos dois invalida a arvore inteira.</summary>
33
12
  private const string Model = "morph-kit-exemplar";
34
13
 
35
- /// <summary>
36
- /// O diretorio do PROJETO de teste, achado subindo a partir do assembly.
37
- /// </summary>
38
- /// <remarks>
39
- /// De proposito: a alternativa seria copiar a cache para <c>bin/</c> e ler a copia — e ai
40
- /// o canario de "um run offline nao suja a arvore commitada" seria verdadeiro por
41
- /// vacuidade, porque a arvore commitada nem teria sido tocada.
42
- /// </remarks>
14
+ // Le a arvore commitada no projeto, nao uma copia em bin/: senao o canario de "nao suja a arvore" seria vacuo.
43
15
  private static string ProjectDirectory
44
16
  {
45
17
  get
@@ -59,7 +31,6 @@ public sealed class KitEvals
59
31
 
60
32
  private static string DatasetRoot => Path.Combine(ProjectDirectory, "eval-datasets");
61
33
 
62
- /// <summary>O pin sob o qual a arvore commitada foi gravada — o mesmo que o carimbo carrega.</summary>
63
34
  private static EvalPin Pin => new(
64
35
  "2026-09-08",
65
36
  new Dictionary<string, string>(StringComparer.Ordinal)
@@ -74,8 +45,6 @@ public sealed class KitEvals
74
45
  {
75
46
  CacheRoot = CacheRoot,
76
47
  DatasetRoot = DatasetRoot,
77
- // FORA da arvore commitada: os resultados carregam latencia, uso de tokens e o flag
78
- // de cache hit, e commita-los seria diff garantido em toda rodada.
79
48
  ResultsRoot = Path.Combine(Path.GetTempPath(), "morph-kit-eval-results"),
80
49
  TimeToLive = TimeSpan.FromDays(3650),
81
50
  ProviderName = Provider,
@@ -89,9 +58,6 @@ public sealed class KitEvals
89
58
  [Trait("Category", "Eval")]
90
59
  public async Task TheKitEvalsRunGreenFromTheCommittedCacheWithNoCredentialAtAll()
91
60
  {
92
- // Nenhuma variavel de credencial e lida por nada nesta montagem — o cliente interno
93
- // e o OfflineChatClient, que nao abre socket e nao le ambiente. Se a cache nao
94
- // respondesse, este teste morreria com EvalCacheMissException nomeando o cenario.
95
61
  var summaryPath = Path.Combine(Path.GetTempPath(), $"morph-kit-eval-{Guid.CreateVersion7():N}.json");
96
62
 
97
63
  try
@@ -102,20 +68,13 @@ public sealed class KitEvals
102
68
  Assert.Equal(EvalCacheState.Warm, summary.Cache);
103
69
  Assert.Equal("ok", summary.Stamp);
104
70
 
105
- // NOMEANDO. `Assert.Equal(0, summary.Failed)` produzia "Expected 0, Actual 1" —
106
- // verdadeiro e inutil: nao dizia qual cenario nem qual regua. A rubrica
107
- // eval-de-modelo.md exige, na banda 9-10, derrubar o cenario NOMEANDO-O.
108
71
  Assert.True(summary.Failed == 0, summary.DescribeFailures());
109
72
  Assert.Empty(summary.Failures);
110
73
  Assert.Equal(summary.Scenarios, summary.Passed);
111
74
 
112
- // A prova central, e ela e um CONTADOR: o cliente interno e o unico caminho para
113
- // a rede, e ele nao foi invocado nenhuma vez.
114
75
  Assert.Equal(0, summary.Misses);
115
76
  Assert.Equal(0, fixture.Offline!.Invocations);
116
77
 
117
- // O dataset nao pode encolher em silencio: uma suite de eval que passa a avaliar
118
- // menos cenarios e a mesma classe de defeito que uma baseline de teste caindo.
119
78
  Assert.Equal(4, summary.Scenarios);
120
79
  }
121
80
  finally
@@ -144,17 +103,10 @@ public sealed class KitEvals
144
103
  [Trait("Category", "Eval")]
145
104
  public void TheCommittedCacheCarriesNoABSOLUTEPathOfTheMachineThatWroteIt()
146
105
  {
147
- // A premissa que torna a arvore commitavel: nada dentro dela aponta para o disco de
148
- // quem gravou. Medido, e nao suposto — se um dia a biblioteca passar a gravar o
149
- // storageRootPath dentro das entradas, este canario morde.
150
106
  var root = ProjectDirectory.Replace('\\', '/');
151
107
  var drive = Path.GetPathRoot(ProjectDirectory)?.Replace('\\', '/');
152
108
 
153
- // A raiz so' identifica uma MAQUINA quando e' uma unidade nomeada ("R:/"). Em POSIX
154
- // `GetPathRoot` devolve "/", que nao identifica nada — e exigir que a cache nao
155
- // contenha "/" reprova qualquer arquivo com um path ou uma URL dentro. O canario
156
- // passava no Windows e era ABSURDO no Linux: media "nenhuma barra em lugar nenhum".
157
- // Medido no CI (job ai-kit, ubuntu): 1 falha, `Assert.DoesNotContain() Sub-string found`.
109
+ // Em POSIX GetPathRoot devolve "/", que nao identifica maquina; so unidade nomeada ("R:/") identifica.
158
110
  var namedDrive = drive is { Length: > 1 } && drive.Contains(':');
159
111
 
160
112
  foreach (var file in Directory.EnumerateFiles(CacheRoot, "*", SearchOption.AllDirectories))
@@ -168,21 +120,6 @@ public sealed class KitEvals
168
120
  }
169
121
  }
170
122
 
171
- /// <summary>
172
- /// Regrava a arvore commitada a partir do fake. NAO roda em CI.
173
- /// </summary>
174
- /// <remarks>
175
- /// <para>
176
- /// Ele existe committed, e nao como um script que alguem rodou uma vez e jogou fora,
177
- /// porque a proxima pessoa que subir o pin precisa saber COMO regravar — e uma instrucao
178
- /// em prosa envelhece; um teste executavel nao.
179
- /// </para>
180
- /// <para>
181
- /// A porta e uma variavel de ambiente, e nao um <c>[Fact(Skip=...)]</c> fixo, porque um
182
- /// skip fixo nunca poderia ser exercitado: seria um seeder que ninguem consegue rodar.
183
- /// Rode com <c>MORPH_EVAL_SEED=1 dotnet test</c> e commite a arvore que sair.
184
- /// </para>
185
- /// </remarks>
186
123
  [Fact]
187
124
  public async Task SeedTheCommittedCacheTree()
188
125
  {
@@ -196,9 +133,7 @@ public sealed class KitEvals
196
133
  MetadataDefaultModelId = Model,
197
134
  };
198
135
 
199
- // Uma resposta por cenario, na ORDEM do dataset (ordinal por caminho relativo):
200
- // orcamento/acme/a-resposta-estruturada, triagem/acme/a-saudacao,
201
- // triagem/acme/b-escalonamento, triagem/globex/a-saudacao.
136
+ // Uma resposta por cenario, na ordem ordinal de caminho do dataset.
202
137
  fake.EnqueueText("""{"valor":"R$ 1.200,00","prazo":"5 dias uteis"}""")
203
138
  .EnqueueText("Bom dia! Como posso ajudar voce hoje?")
204
139
  .EnqueueText("Encaminhando voce para um atendente humano agora.")
@@ -4,9 +4,6 @@ using Xunit;
4
4
 
5
5
  namespace Morph.AiKit.Tests.Evals;
6
6
 
7
- /// <summary>
8
- /// O cliente que RECUSA: a peca de que depende "o eval roda no CI sem credencial".
9
- /// </summary>
10
7
  public sealed class OfflineChatClientTests
11
8
  {
12
9
  private static OfflineChatClient Build(Func<string>? scenario = null)
@@ -19,13 +16,9 @@ public sealed class OfflineChatClientTests
19
16
 
20
17
  var metadata = Assert.IsType<ChatClientMetadata>(client.GetService(typeof(ChatClientMetadata)));
21
18
 
22
- // Estes DOIS campos entram na chave de cache. Se divergissem do run que gravou, todo
23
- // cenario daria miss e a cache quente seria indistinguivel de uma cache vazia.
24
19
  Assert.Equal("morph-fake", metadata.ProviderName);
25
20
  Assert.Equal("modelo-exemplar", metadata.DefaultModelId);
26
21
 
27
- // E o endpoint e NULO de proposito: ele nao entra na chave (medido na v10.9.0), e um
28
- // cliente que nunca liga para lugar nenhum nao deve anunciar um destino.
29
22
  Assert.Null(metadata.ProviderUri);
30
23
  }
31
24
 
@@ -59,9 +52,7 @@ public sealed class OfflineChatClientTests
59
52
  [Fact]
60
53
  public void TheScenarioNameIsReadAtCALLTIME_NotCaptured()
61
54
  {
62
- // Um fixture roda N cenarios com o MESMO cliente. Se o nome fosse capturado na
63
- // construcao, toda mensagem de miss nomearia o primeiro cenario e mandaria o leitor
64
- // investigar o arquivo errado.
55
+ // O fixture roda N cenarios com o mesmo cliente: o nome do cenario nao pode ser capturado na construcao.
65
56
  var current = "primeiro";
66
57
  using var client = Build(() => current);
67
58
 
@@ -3,30 +3,6 @@ using Microsoft.Extensions.AI;
3
3
 
4
4
  namespace Morph.AiKit.Tests.Fakes;
5
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
6
  public sealed class FakeChatClient : IChatClient
31
7
  {
32
8
  private readonly Lock _gate = new();
@@ -34,12 +10,10 @@ public sealed class FakeChatClient : IChatClient
34
10
  private readonly List<ChatOptions?> _receivedOptions = [];
35
11
  private readonly List<IReadOnlyList<ChatMessage>> _receivedMessages = [];
36
12
 
37
- /// <summary>Cria um fake com a fila vazia. Enfileire antes de usar.</summary>
38
13
  public FakeChatClient()
39
14
  {
40
15
  }
41
16
 
42
- /// <summary>Cria um fake ja roteirizado com as respostas dadas, na ordem.</summary>
43
17
  public FakeChatClient(params ChatResponse[] scripted)
44
18
  {
45
19
  ArgumentNullException.ThrowIfNull(scripted);
@@ -50,7 +24,6 @@ public sealed class FakeChatClient : IChatClient
50
24
  }
51
25
  }
52
26
 
53
- /// <summary>Atalho: uma resposta de assistente por texto, na ordem dada.</summary>
54
27
  public static FakeChatClient Scripted(params string[] texts)
55
28
  {
56
29
  ArgumentNullException.ThrowIfNull(texts);
@@ -64,51 +37,35 @@ public sealed class FakeChatClient : IChatClient
64
37
  return client;
65
38
  }
66
39
 
67
- /// <summary>Quantas vezes o cliente foi invocado (streaming incluso).</summary>
68
40
  public int CallCount { get; private set; }
69
41
 
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>
42
+ // ProviderName e DefaultModelId entram na chave da cache de eval: o OfflineChatClient precisa anunciar os mesmos.
76
43
  public string MetadataProviderName { get; init; } = "fake";
77
44
 
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
45
  public string? MetadataDefaultModelId { get; init; }
83
46
 
84
- /// <summary>Se <see cref="Dispose"/> ja foi chamado — usado para provar ownership de cache.</summary>
85
47
  public bool IsDisposed { get; private set; }
86
48
 
87
- /// <summary>As <see cref="ChatOptions"/> recebidas, uma entrada por chamada, na ordem.</summary>
88
49
  public IReadOnlyList<ChatOptions?> ReceivedOptions
89
50
  {
90
51
  get { lock (_gate) { return [.. _receivedOptions]; } }
91
52
  }
92
53
 
93
- /// <summary>As mensagens recebidas, uma entrada por chamada, na ordem.</summary>
94
54
  public IReadOnlyList<IReadOnlyList<ChatMessage>> ReceivedMessages
95
55
  {
96
56
  get { lock (_gate) { return [.. _receivedMessages]; } }
97
57
  }
98
58
 
99
- /// <summary>As opcoes da ultima chamada, ou nulo se nunca foi chamado.</summary>
100
59
  public ChatOptions? LastOptions
101
60
  {
102
61
  get { lock (_gate) { return _receivedOptions.Count == 0 ? null : _receivedOptions[^1]; } }
103
62
  }
104
63
 
105
- /// <summary>As mensagens da ultima chamada, ou nulo se nunca foi chamado.</summary>
106
64
  public IReadOnlyList<ChatMessage>? LastMessages
107
65
  {
108
66
  get { lock (_gate) { return _receivedMessages.Count == 0 ? null : _receivedMessages[^1]; } }
109
67
  }
110
68
 
111
- /// <summary>Enfileira uma resposta ja montada. Devolve o proprio fake, para encadear.</summary>
112
69
  public FakeChatClient Enqueue(ChatResponse response)
113
70
  {
114
71
  ArgumentNullException.ThrowIfNull(response);
@@ -121,18 +78,9 @@ public sealed class FakeChatClient : IChatClient
121
78
  return this;
122
79
  }
123
80
 
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
81
  public FakeChatClient EnqueueText(string text, UsageDetails? usage = null)
130
82
  => Enqueue(new ChatResponse(new ChatMessage(ChatRole.Assistant, text)) { Usage = usage });
131
83
 
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
84
  public FakeChatClient EnqueueTextWithUsage(
137
85
  string text,
138
86
  long inputTokens,
@@ -148,7 +96,6 @@ public sealed class FakeChatClient : IChatClient
148
96
  CachedInputTokenCount = cachedInputTokens,
149
97
  });
150
98
 
151
- /// <inheritdoc />
152
99
  public Task<ChatResponse> GetResponseAsync(
153
100
  IEnumerable<ChatMessage> messages,
154
101
  ChatOptions? options = null,
@@ -160,7 +107,6 @@ public sealed class FakeChatClient : IChatClient
160
107
  return Task.FromResult(Record(messages, options));
161
108
  }
162
109
 
163
- /// <inheritdoc />
164
110
  public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
165
111
  IEnumerable<ChatMessage> messages,
166
112
  ChatOptions? options = null,
@@ -180,7 +126,6 @@ public sealed class FakeChatClient : IChatClient
180
126
  await Task.CompletedTask.ConfigureAwait(false);
181
127
  }
182
128
 
183
- /// <inheritdoc />
184
129
  public object? GetService(Type serviceType, object? serviceKey = null)
185
130
  {
186
131
  ArgumentNullException.ThrowIfNull(serviceType);
@@ -198,7 +143,6 @@ public sealed class FakeChatClient : IChatClient
198
143
  return serviceType.IsInstanceOfType(this) ? this : null;
199
144
  }
200
145
 
201
- /// <inheritdoc />
202
146
  public void Dispose() => IsDisposed = true;
203
147
 
204
148
  private ChatResponse Record(IEnumerable<ChatMessage> messages, ChatOptions? options)
@@ -4,17 +4,6 @@ using Xunit;
4
4
 
5
5
  namespace Morph.AiKit.Tests.Fakes;
6
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
7
  public sealed class FakeChatClientTests
19
8
  {
20
9
  [Fact]
@@ -114,8 +103,6 @@ public sealed class FakeChatClientTests
114
103
  [Fact]
115
104
  public async Task UsageIsControlledByTheTestAndSurvivesTheRoundTrip()
116
105
  {
117
- // E assim que a T11 vai verificar tarifa sem rede: Usage conhecido entra,
118
- // Usage conhecido sai.
119
106
  using var client = new FakeChatClient();
120
107
  client.EnqueueTextWithUsage("ok", inputTokens: 1_000, outputTokens: 250, cachedInputTokens: 400);
121
108
 
@@ -155,17 +142,6 @@ public sealed class FakeChatClientTests
155
142
  [Fact]
156
143
  public async Task TheWholeSeamAnswersFromMemoryAloneWithNoConfigurationAndNoConnection()
157
144
  {
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
145
  var document = """
170
146
  {
171
147
  "version": "2.0",
@@ -195,8 +171,6 @@ public sealed class FakeChatClientTests
195
171
  [Fact]
196
172
  public async Task ConcurrentCallersNeverLoseACallOrACapture()
197
173
  {
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
174
  using var client = new FakeChatClient();
201
175
  for (var i = 0; i < 50; i++)
202
176
  {
@@ -1,30 +1,12 @@
1
1
  namespace Morph.AiKit.Tests.Fakes;
2
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>
3
+ // Escrito a mao: Microsoft.Extensions.TimeProvider.Testing nao esta no pin (framework/ai-pin.json).
20
4
  public sealed class FakeClock(DateTimeOffset start) : TimeProvider
21
5
  {
22
6
  private DateTimeOffset _now = start;
23
7
 
24
- /// <inheritdoc />
25
8
  public override DateTimeOffset GetUtcNow() => _now;
26
9
 
27
- /// <summary>Avanca o relogio. Rejeita andar para tras: o tempo do teste e monotono.</summary>
28
10
  public void Advance(TimeSpan delta)
29
11
  {
30
12
  ArgumentOutOfRangeException.ThrowIfLessThan(delta, TimeSpan.Zero);