@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.
- package/CLAUDE.md +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +28 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- 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
|
+
}
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs
ADDED
|
@@ -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
|
+
}
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs
ADDED
|
@@ -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
|
+
}
|