@polymorphism-tech/morph-spec 8.38.0 → 8.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +19 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +5 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +3 -5
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -1,55 +1,14 @@
|
|
|
1
1
|
namespace Morph.AiKit.Rag;
|
|
2
2
|
|
|
3
|
-
/// <summary>
|
|
4
|
-
/// O que precisa acontecer no acervo para que ele passe a refletir a entrada — e nada mais.
|
|
5
|
-
/// </summary>
|
|
6
|
-
/// <remarks>
|
|
7
|
-
/// <para>
|
|
8
|
-
/// <b>O defeito que este tipo existe para matar</b> e o apaga-e-regrava: a base de
|
|
9
|
-
/// conhecimento do GHLBrain <b>apaga e re-embeda o documento inteiro</b> a cada ingestao,
|
|
10
|
-
/// mudou ou nao. O custo e pago em embedding de trecho que nao mudou e em latencia (uma
|
|
11
|
-
/// chamada de embedding por item no reindex em massa). Um plano em quatro baldes torna
|
|
12
|
-
/// visivel o que a ingestao inteira escondia: quase sempre <see cref="Unchanged"/> e a lista
|
|
13
|
-
/// grande e as outras tres sao curtas.
|
|
14
|
-
/// </para>
|
|
15
|
-
/// <para>
|
|
16
|
-
/// Errata (2026-09-15, Morph.GHLBrain#96): a versao anterior deste comentario citava tambem
|
|
17
|
-
/// uma janela entre o apagar e o gravar em que a busca nao acha nada. No GHLBrain o apagar e
|
|
18
|
-
/// o gravar ja rodam numa transacao so, com o embedding antes dela, entao essa janela nao
|
|
19
|
-
/// existe la. Ela existe em quem apaga fora da transacao que grava.
|
|
20
|
-
/// </para>
|
|
21
|
-
/// <para>
|
|
22
|
-
/// <b>E um plano, nao uma execucao.</b> Nada aqui toca banco, vetor ou arquivo: quem aplica
|
|
23
|
-
/// e o projeto, que sabe qual e a sua transacao. E por isso que a idempotencia da ingestao
|
|
24
|
-
/// pode ser provada sem subir infraestrutura nenhuma.
|
|
25
|
-
/// </para>
|
|
26
|
-
/// </remarks>
|
|
27
|
-
/// <typeparam name="T">O item ingerido — documento, trecho, linha.</typeparam>
|
|
28
|
-
/// <param name="Added">Itens cuja chave nao existia no acervo.</param>
|
|
29
|
-
/// <param name="Updated">Itens cuja chave existia com <b>outro</b> conteudo.</param>
|
|
30
|
-
/// <param name="Removed">
|
|
31
|
-
/// Chaves que estavam no acervo e <b>nao vieram</b> na entrada. Sao chaves, e nao itens:
|
|
32
|
-
/// o que sumiu da origem nao tem corpo para carregar.
|
|
33
|
-
/// </param>
|
|
34
|
-
/// <param name="Unchanged">
|
|
35
|
-
/// Itens cuja chave existia com o <b>mesmo</b> conteudo. Nao re-embedar estes e a economia
|
|
36
|
-
/// inteira desta feature.
|
|
37
|
-
/// </param>
|
|
38
3
|
public sealed record DeltaPlan<T>(
|
|
39
4
|
IReadOnlyList<T> Added,
|
|
40
5
|
IReadOnlyList<T> Updated,
|
|
41
6
|
IReadOnlyList<string> Removed,
|
|
42
7
|
IReadOnlyList<T> Unchanged)
|
|
43
8
|
{
|
|
44
|
-
/// <summary>Plano vazio — nem entrada, nem acervo.</summary>
|
|
45
9
|
public static DeltaPlan<T> Empty { get; } = new([], [], [], []);
|
|
46
10
|
|
|
47
|
-
/// <summary>
|
|
48
|
-
/// Se o acervo ja reflete a entrada. <b>E a idempotencia expressa como predicado:</b>
|
|
49
|
-
/// re-planejar a mesma entrada sobre o acervo que ela produziu tem de dar verdadeiro.
|
|
50
|
-
/// </summary>
|
|
51
11
|
public bool IsNoOp => Added.Count == 0 && Updated.Count == 0 && Removed.Count == 0;
|
|
52
12
|
|
|
53
|
-
/// <summary>Quantos itens da entrada tocam o acervo. Zero num plano idempotente.</summary>
|
|
54
13
|
public int WriteCount => Added.Count + Updated.Count + Removed.Count;
|
|
55
14
|
}
|
|
@@ -8,23 +8,8 @@ using Xunit;
|
|
|
8
8
|
|
|
9
9
|
namespace Morph.AiKit.Tests;
|
|
10
10
|
|
|
11
|
-
/// <summary>
|
|
12
|
-
/// Canarios da <see cref="AgentFactory"/> (T10). Duas afirmacoes sao o coracao do arquivo:
|
|
13
|
-
/// <b>a precedencia do protocolo</b> (spec vence o alias quando presente, alias vence quando
|
|
14
|
-
/// a spec cala) e <b>a ordem do pipeline</b>, observada na sequencia real de chamadas, nao
|
|
15
|
-
/// na leitura do codigo.
|
|
16
|
-
/// </summary>
|
|
17
|
-
/// <remarks>
|
|
18
|
-
/// Todo <c>await</c> passa <c>TestContext.Current.CancellationToken</c> — exigencia do
|
|
19
|
-
/// analisador xUnit1051, que sob <c>TreatWarningsAsErrors</c> e build vermelho.
|
|
20
|
-
/// </remarks>
|
|
21
11
|
public sealed class AgentFactoryTests
|
|
22
12
|
{
|
|
23
|
-
// ---------------------------------------------------------------------------------
|
|
24
|
-
// Documentos de registry usados pelos casos
|
|
25
|
-
// ---------------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
/// <summary>Um alias Responses e um alias Chat COM o limite do GPT-5.4 declarado.</summary>
|
|
28
13
|
private const string TwoProtocolsDocument = """
|
|
29
14
|
{
|
|
30
15
|
"version": "2.0",
|
|
@@ -53,14 +38,6 @@ public sealed class AgentFactoryTests
|
|
|
53
38
|
private static AgentSpec Spec(string alias = "responses-alias", AgentApi? api = null)
|
|
54
39
|
=> new() { Alias = alias, Instructions = "voce e um agente de teste", Api = api };
|
|
55
40
|
|
|
56
|
-
// ---------------------------------------------------------------------------------
|
|
57
|
-
// doneCriteria 1 — a precedencia do api:
|
|
58
|
-
// ---------------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/// <summary>
|
|
61
|
-
/// Spec calada: vale o <c>api:</c> do alias. O seletor de cliente e o observatorio —
|
|
62
|
-
/// ele recebe o protocolo <b>efetivo</b>, ja resolvido.
|
|
63
|
-
/// </summary>
|
|
64
41
|
[Fact]
|
|
65
42
|
public void TheAliasApiWinsWhenTheSpecDeclaresNone()
|
|
66
43
|
{
|
|
@@ -77,11 +54,6 @@ public sealed class AgentFactoryTests
|
|
|
77
54
|
Assert.Equal([AgentApi.Responses, AgentApi.Chat], seen);
|
|
78
55
|
}
|
|
79
56
|
|
|
80
|
-
/// <summary>
|
|
81
|
-
/// Spec preenchida: ela vence, nos dois sentidos. A regra nao e reimplementada aqui —
|
|
82
|
-
/// a fabrica chama <see cref="AgentSpec.ResolveApi"/> (T7), e este teste morre se
|
|
83
|
-
/// alguem inverter a precedencia la ou copia-la para ca.
|
|
84
|
-
/// </summary>
|
|
85
57
|
[Fact]
|
|
86
58
|
public void TheSpecApiWinsOverTheAliasInBothDirections()
|
|
87
59
|
{
|
|
@@ -98,11 +70,6 @@ public sealed class AgentFactoryTests
|
|
|
98
70
|
Assert.Equal([AgentApi.Chat, AgentApi.Responses], seen);
|
|
99
71
|
}
|
|
100
72
|
|
|
101
|
-
/// <summary>
|
|
102
|
-
/// Sem seletor, "a spec vence" nao pode ser cumprido: o registry memoiza UM cliente por
|
|
103
|
-
/// alias, construido para o protocolo do alias. A fabrica recusa nomeando os dois
|
|
104
|
-
/// protocolos, em vez de devolver um agente que diz falar um e fala outro.
|
|
105
|
-
/// </summary>
|
|
106
73
|
[Fact]
|
|
107
74
|
public void WithoutASelectorADivergentSpecApiIsRefusedNamingBothProtocols()
|
|
108
75
|
{
|
|
@@ -118,10 +85,6 @@ public sealed class AgentFactoryTests
|
|
|
118
85
|
Assert.Contains("responses-alias", error.Message, StringComparison.Ordinal);
|
|
119
86
|
}
|
|
120
87
|
|
|
121
|
-
/// <summary>
|
|
122
|
-
/// Sem divergencia e sem seletor, o caminho normal: o cliente memoizado do alias, uma
|
|
123
|
-
/// unica construcao por alias, por mais agentes que se criem.
|
|
124
|
-
/// </summary>
|
|
125
88
|
[Fact]
|
|
126
89
|
public void WithoutASelectorEveryAgentOfAnAliasSharesTheMemoizedClient()
|
|
127
90
|
{
|
|
@@ -135,20 +98,6 @@ public sealed class AgentFactoryTests
|
|
|
135
98
|
Assert.Equal(1, providerFactory.CreateCount);
|
|
136
99
|
}
|
|
137
100
|
|
|
138
|
-
// ---------------------------------------------------------------------------------
|
|
139
|
-
// doneCriteria 2 — a ordem do pipeline, observada nas chamadas
|
|
140
|
-
// ---------------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
/// <summary>
|
|
143
|
-
/// A ordem completa, medida numa invocacao com <b>duas</b> voltas de tool calling.
|
|
144
|
-
/// </summary>
|
|
145
|
-
/// <remarks>
|
|
146
|
-
/// O que a sequencia prova, e que nenhuma leitura de codigo prova: o encaixe de custo
|
|
147
|
-
/// entra e sai <b>uma</b> vez envolvendo as duas voltas (esta ACIMA do
|
|
148
|
-
/// <c>FunctionInvokingChatClient</c>), e o encaixe de telemetria entra e sai <b>duas</b>
|
|
149
|
-
/// vezes, uma por chamada ao modelo (esta ABAIXO). Trocar os dois de lugar muda esta
|
|
150
|
-
/// lista, e o teste morre.
|
|
151
|
-
/// </remarks>
|
|
152
101
|
[Fact]
|
|
153
102
|
public async Task ThePipelineOrderIsUsageCostAboveFunctionInvocationAndTelemetryBelowIt()
|
|
154
103
|
{
|
|
@@ -180,12 +129,7 @@ public sealed class AgentFactoryTests
|
|
|
180
129
|
log);
|
|
181
130
|
}
|
|
182
131
|
|
|
183
|
-
|
|
184
|
-
/// A razao de o custo ficar acima do FICC, em numeros: ele ve UMA resposta com o uso
|
|
185
|
-
/// somado das duas voltas (10+30 de entrada, 4+6 de saida). Abaixo do FICC veria duas
|
|
186
|
-
/// respostas parciais — e leria um <c>UsageDetails</c> que o proprio FICC muta para
|
|
187
|
-
/// acumular, o que faria a mesma conta sair diferente conforme a hora de ler.
|
|
188
|
-
/// </summary>
|
|
132
|
+
// Abaixo do FunctionInvokingChatClient o custo leria o UsageDetails que ele muta para acumular entre voltas.
|
|
189
133
|
[Fact]
|
|
190
134
|
public async Task UsageCostAboveFunctionInvocationSeesTheUsageOfEveryToolRoundSummedOnce()
|
|
191
135
|
{
|
|
@@ -209,15 +153,6 @@ public sealed class AgentFactoryTests
|
|
|
209
153
|
Assert.Equal(10, usage.OutputTokenCount);
|
|
210
154
|
}
|
|
211
155
|
|
|
212
|
-
// ---------------------------------------------------------------------------------
|
|
213
|
-
// As opcoes de nivel de agente (T7) chegam ao lugar que a T7 declarou
|
|
214
|
-
// ---------------------------------------------------------------------------------
|
|
215
|
-
|
|
216
|
-
/// <summary>
|
|
217
|
-
/// <c>MaxToolIterations</c> e <c>AllowConcurrentToolInvocation</c> tem destino nomeado
|
|
218
|
-
/// no contrato da T7. Aqui eles sao lidos <b>do objeto real</b> do pipeline, via
|
|
219
|
-
/// <c>GetService</c> — nao de um campo que a fabrica tenha guardado.
|
|
220
|
-
/// </summary>
|
|
221
156
|
[Fact]
|
|
222
157
|
public void TheAgentLevelKnobsReachTheFunctionInvokingChatClient()
|
|
223
158
|
{
|
|
@@ -236,10 +171,6 @@ public sealed class AgentFactoryTests
|
|
|
236
171
|
Assert.True(functionInvoking.AllowConcurrentInvocation);
|
|
237
172
|
}
|
|
238
173
|
|
|
239
|
-
/// <summary>
|
|
240
|
-
/// <c>TimeoutSeconds</c> vira <see cref="CancellationToken"/> de verdade: um cliente que
|
|
241
|
-
/// nunca responde e cancelado, em vez de pendurar a invocacao.
|
|
242
|
-
/// </summary>
|
|
243
174
|
[Fact]
|
|
244
175
|
public async Task TimeoutSecondsBecomesACancellationTokenThatActuallyCancels()
|
|
245
176
|
{
|
|
@@ -260,15 +191,6 @@ public sealed class AgentFactoryTests
|
|
|
260
191
|
() => agent.RunAsync("oi", session, cancellationToken: TestContext.Current.CancellationToken));
|
|
261
192
|
}
|
|
262
193
|
|
|
263
|
-
// ---------------------------------------------------------------------------------
|
|
264
|
-
// A spec inteira chega as ChatOptions do agente
|
|
265
|
-
// ---------------------------------------------------------------------------------
|
|
266
|
-
|
|
267
|
-
/// <summary>
|
|
268
|
-
/// O modelo vem do alias (a spec nunca cita modelo), as instrucoes e o schema vem da
|
|
269
|
-
/// spec, e <see cref="AgentSpecOptions.ApplyTo"/> nao atropela nem as tools nem o
|
|
270
|
-
/// formato de resposta ja definidos.
|
|
271
|
-
/// </summary>
|
|
272
194
|
[Fact]
|
|
273
195
|
public async Task TheSpecReachesTheChatOptionsWithoutClobberingToolsOrSchema()
|
|
274
196
|
{
|
|
@@ -304,14 +226,6 @@ public sealed class AgentFactoryTests
|
|
|
304
226
|
Assert.Equal("agente-de-teste", agent.Name);
|
|
305
227
|
}
|
|
306
228
|
|
|
307
|
-
// ---------------------------------------------------------------------------------
|
|
308
|
-
// Recusas
|
|
309
|
-
// ---------------------------------------------------------------------------------
|
|
310
|
-
|
|
311
|
-
/// <summary>
|
|
312
|
-
/// Limite de alias violado nao vira agente. A recusa carrega a procedencia escrita no
|
|
313
|
-
/// registry — sem ela, o limite nao teria sequer carregado (T8).
|
|
314
|
-
/// </summary>
|
|
315
229
|
[Fact]
|
|
316
230
|
public void AViolatedAliasConstraintRefusesTheAgentAndCarriesItsProvenance()
|
|
317
231
|
{
|
|
@@ -328,11 +242,6 @@ public sealed class AgentFactoryTests
|
|
|
328
242
|
Assert.Contains("issue #93", error.Message, StringComparison.Ordinal);
|
|
329
243
|
}
|
|
330
244
|
|
|
331
|
-
/// <summary>
|
|
332
|
-
/// O MESMO alias, sem tools, nao viola nada: o limite tem quatro fatos, e tres nao
|
|
333
|
-
/// bastam. O canario existe para que a recusa acima nao seja confundida com "modelo de
|
|
334
|
-
/// raciocinio rejeita tool".
|
|
335
|
-
/// </summary>
|
|
336
245
|
[Fact]
|
|
337
246
|
public void TheSameAliasWithoutToolsCreatesTheAgentNormally()
|
|
338
247
|
{
|
|
@@ -347,7 +256,6 @@ public sealed class AgentFactoryTests
|
|
|
347
256
|
Assert.NotNull(agent);
|
|
348
257
|
}
|
|
349
258
|
|
|
350
|
-
/// <summary>Alias desconhecido falha na fabrica com o mesmo erro do registry.</summary>
|
|
351
259
|
[Fact]
|
|
352
260
|
public void AnUnknownAliasFailsAtCreateTime()
|
|
353
261
|
{
|
|
@@ -359,7 +267,6 @@ public sealed class AgentFactoryTests
|
|
|
359
267
|
Assert.Equal(ModelRegistryError.UnknownAlias, error.Reason);
|
|
360
268
|
}
|
|
361
269
|
|
|
362
|
-
/// <summary>Seletor que devolve nulo e recusa explicita, nunca agente sem cliente.</summary>
|
|
363
270
|
[Fact]
|
|
364
271
|
public void ASelectorThatReturnsNullIsRefusedInsteadOfProducingAClientlessAgent()
|
|
365
272
|
{
|
|
@@ -374,10 +281,6 @@ public sealed class AgentFactoryTests
|
|
|
374
281
|
Assert.Equal(AgentFactoryError.NoChatClient, error.Reason);
|
|
375
282
|
}
|
|
376
283
|
|
|
377
|
-
/// <summary>
|
|
378
|
-
/// Sem nenhum encaixe preenchido, o agente e valido e responde. Um agente sem custo e
|
|
379
|
-
/// sem telemetria nao e um agente pela metade.
|
|
380
|
-
/// </summary>
|
|
381
284
|
[Fact]
|
|
382
285
|
public async Task AnAgentWithNoHooksIsStillAValidAgentThatAnswers()
|
|
383
286
|
{
|
|
@@ -395,15 +298,7 @@ public sealed class AgentFactoryTests
|
|
|
395
298
|
Assert.Equal("pronto", response.Text);
|
|
396
299
|
}
|
|
397
300
|
|
|
398
|
-
//
|
|
399
|
-
// Apoio dos testes
|
|
400
|
-
// ---------------------------------------------------------------------------------
|
|
401
|
-
|
|
402
|
-
/// <summary>
|
|
403
|
-
/// Roteiro de duas voltas: o modelo pede a tool, a tool responde, o modelo conclui. Os
|
|
404
|
-
/// numeros de uso sao escolhidos para que a soma (40/10) seja distinguivel de qualquer
|
|
405
|
-
/// uma das parcelas.
|
|
406
|
-
/// </summary>
|
|
301
|
+
// Os usos por volta (10/4 e 30/6) diferem da soma (40/10), para a soma nao se confundir com uma parcela.
|
|
407
302
|
private static FakeChatClient ScriptedToolCallThenAnswer()
|
|
408
303
|
=> new FakeChatClient()
|
|
409
304
|
.Enqueue(new ChatResponse(new ChatMessage(
|
|
@@ -433,10 +328,6 @@ public sealed class AgentFactoryTests
|
|
|
433
328
|
return factory.Create(spec);
|
|
434
329
|
}
|
|
435
330
|
|
|
436
|
-
/// <summary>
|
|
437
|
-
/// Marca a entrada e a saida de uma posicao do pipeline. E o unico jeito de observar a
|
|
438
|
-
/// ORDEM: ela nao esta em nenhuma propriedade, so na sequencia das chamadas.
|
|
439
|
-
/// </summary>
|
|
440
331
|
private sealed class ProbeChatClient(
|
|
441
332
|
IChatClient inner,
|
|
442
333
|
string tag,
|
|
@@ -465,7 +356,6 @@ public sealed class AgentFactoryTests
|
|
|
465
356
|
}
|
|
466
357
|
}
|
|
467
358
|
|
|
468
|
-
/// <summary>Cliente que nunca responde — existe so para o timeout ter o que cancelar.</summary>
|
|
469
359
|
private sealed class NeverAnsweringChatClient : IChatClient
|
|
470
360
|
{
|
|
471
361
|
public async Task<ChatResponse> GetResponseAsync(
|
|
@@ -7,24 +7,8 @@ using Xunit;
|
|
|
7
7
|
|
|
8
8
|
namespace Morph.AiKit.Tests;
|
|
9
9
|
|
|
10
|
-
/// <summary>
|
|
11
|
-
/// Canarios do agente-como-dado (T7). O que estes testes protegem nao e a existencia dos
|
|
12
|
-
/// tipos — o compilador ja faz isso — e sim a UNICA regra estrutural que
|
|
13
|
-
/// <see cref="AgentSpecOptions"/> tem: <b>nulo nao envia nada ao provider</b>.
|
|
14
|
-
/// </summary>
|
|
15
|
-
/// <remarks>
|
|
16
|
-
/// A comparacao e feita por reflexao contra um <see cref="ChatOptions"/> virgem, e nao
|
|
17
|
-
/// contra uma lista de propriedades escrita a mao, de proposito: uma propriedade nova no
|
|
18
|
-
/// <c>Microsoft.Extensions.AI</c> entra na comparacao sozinha. Uma lista escrita a mao
|
|
19
|
-
/// envelheceria em silencio no proximo bump do pin — que e exatamente o defeito que a
|
|
20
|
-
/// feature inteira existe para matar.
|
|
21
|
-
/// </remarks>
|
|
22
10
|
public sealed class AgentSpecTests
|
|
23
11
|
{
|
|
24
|
-
// ---------------------------------------------------------------------------------
|
|
25
|
-
// A regra estrutural: nulo = nao envia
|
|
26
|
-
// ---------------------------------------------------------------------------------
|
|
27
|
-
|
|
28
12
|
[Fact]
|
|
29
13
|
public void FreshOptionsProduceChatOptionsIndistinguishableFromPristine()
|
|
30
14
|
{
|
|
@@ -68,9 +52,7 @@ public sealed class AgentSpecTests
|
|
|
68
52
|
[Fact]
|
|
69
53
|
public void AgentLevelKnobsNeverLeakIntoChatOptions()
|
|
70
54
|
{
|
|
71
|
-
// As quatro opcoes de nivel de agente
|
|
72
|
-
// lidas pela AgentFactory (T10) e pela telemetria (T12). Se alguem "resolver" isso
|
|
73
|
-
// despejando-as em AdditionalProperties, este teste morre — que e o ponto.
|
|
55
|
+
// As quatro opcoes de nivel de agente nao tem casa em ChatOptions: a AgentFactory e a telemetria as leem.
|
|
74
56
|
var produced = new AgentSpecOptions
|
|
75
57
|
{
|
|
76
58
|
MaxToolIterations = 7,
|
|
@@ -86,10 +68,6 @@ public sealed class AgentSpecTests
|
|
|
86
68
|
$"Opcao de nivel de agente vazou para ChatOptions: {string.Join(", ", differing)}");
|
|
87
69
|
}
|
|
88
70
|
|
|
89
|
-
// ---------------------------------------------------------------------------------
|
|
90
|
-
// Mapeamento de cada opcao de nivel de requisicao para a sua casa
|
|
91
|
-
// ---------------------------------------------------------------------------------
|
|
92
|
-
|
|
93
71
|
[Fact]
|
|
94
72
|
public void EachRequestLevelOptionLandsInItsOwnChatOptionsHome()
|
|
95
73
|
{
|
|
@@ -115,8 +93,7 @@ public sealed class AgentSpecTests
|
|
|
115
93
|
Assert.Same(ChatToolMode.RequireAny, produced.ToolMode);
|
|
116
94
|
Assert.Equal(ReasoningEffort.Medium, produced.Reasoning?.Effort);
|
|
117
95
|
|
|
118
|
-
//
|
|
119
|
-
// por reflexao sobre o assembly do pin) — viaja em AdditionalProperties.
|
|
96
|
+
// ServiceTier nao existe em ChatOptions no Microsoft.Extensions.AI 10.9.0: viaja em AdditionalProperties.
|
|
120
97
|
Assert.NotNull(produced.AdditionalProperties);
|
|
121
98
|
Assert.True(produced.AdditionalProperties.TryGetValue(
|
|
122
99
|
AgentSpecOptions.ServiceTierPropertyName,
|
|
@@ -127,8 +104,6 @@ public sealed class AgentSpecTests
|
|
|
127
104
|
[Fact]
|
|
128
105
|
public void AnExplicitlyEmptyStopSequenceListIsSentAsWritten()
|
|
129
106
|
{
|
|
130
|
-
// "Nulo nao envia; nao-nulo envia o que voce escreveu." Tratar lista vazia como
|
|
131
|
-
// nula seria uma excecao escondida na unica regra do tipo.
|
|
132
107
|
var produced = new AgentSpecOptions { StopSequences = [] }.ToChatOptions();
|
|
133
108
|
|
|
134
109
|
Assert.Equal([nameof(ChatOptions.StopSequences)], DiffAgainstPristine(produced));
|
|
@@ -139,7 +114,6 @@ public sealed class AgentSpecTests
|
|
|
139
114
|
[Fact]
|
|
140
115
|
public void ApplyToNeverClobbersWhatTheCallerAlreadySet()
|
|
141
116
|
{
|
|
142
|
-
// A AgentFactory (T10) preenche Tools e ResponseFormat; ApplyTo nao pode apaga-los.
|
|
143
117
|
var destination = new ChatOptions
|
|
144
118
|
{
|
|
145
119
|
ResponseFormat = ChatResponseFormat.Json,
|
|
@@ -158,10 +132,6 @@ public sealed class AgentSpecTests
|
|
|
158
132
|
public void ApplyToRejectsANullDestination()
|
|
159
133
|
=> Assert.Throws<ArgumentNullException>(() => new AgentSpecOptions().ApplyTo(null!));
|
|
160
134
|
|
|
161
|
-
// ---------------------------------------------------------------------------------
|
|
162
|
-
// AgentSpec
|
|
163
|
-
// ---------------------------------------------------------------------------------
|
|
164
|
-
|
|
165
135
|
[Fact]
|
|
166
136
|
public void AFreshSpecCarriesNoToolsNoSchemaAndSendsNoOptions()
|
|
167
137
|
{
|
|
@@ -194,11 +164,9 @@ public sealed class AgentSpecTests
|
|
|
194
164
|
var withoutOverride = new AgentSpec { Alias = "a", Instructions = "i" };
|
|
195
165
|
var withOverride = withoutOverride with { Api = AgentApi.Chat };
|
|
196
166
|
|
|
197
|
-
// alias manda quando a spec nao opinou
|
|
198
167
|
Assert.Equal(AgentApi.Responses, withoutOverride.ResolveApi(AgentApi.Responses));
|
|
199
168
|
Assert.Equal(AgentApi.Chat, withoutOverride.ResolveApi(AgentApi.Chat));
|
|
200
169
|
|
|
201
|
-
// spec manda quando opinou — inclusive contra o alias
|
|
202
170
|
Assert.Equal(AgentApi.Chat, withOverride.ResolveApi(AgentApi.Responses));
|
|
203
171
|
}
|
|
204
172
|
|
|
@@ -222,15 +190,6 @@ public sealed class AgentSpecTests
|
|
|
222
190
|
Assert.Equal("object", spec.OutputSchema.Value.GetProperty("type").GetString());
|
|
223
191
|
}
|
|
224
192
|
|
|
225
|
-
// ---------------------------------------------------------------------------------
|
|
226
|
-
// Helpers
|
|
227
|
-
// ---------------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
/// <summary>
|
|
230
|
-
/// Nomes das propriedades de <see cref="ChatOptions"/> em que <paramref name="actual"/>
|
|
231
|
-
/// difere de um <see cref="ChatOptions"/> virgem — ou seja, o conjunto exato do que
|
|
232
|
-
/// seria enviado ao provider.
|
|
233
|
-
/// </summary>
|
|
234
193
|
private static IReadOnlyList<string> DiffAgainstPristine(ChatOptions actual)
|
|
235
194
|
{
|
|
236
195
|
var pristine = new ChatOptions();
|
|
@@ -7,21 +7,6 @@ using Xunit;
|
|
|
7
7
|
|
|
8
8
|
namespace Morph.AiKit.Tests;
|
|
9
9
|
|
|
10
|
-
/// <summary>
|
|
11
|
-
/// Canarios do <see cref="ConversationStore"/> (T13). A afirmacao central: <b>uma unica
|
|
12
|
-
/// instancia de provider atende todas as sessoes, e o historico de uma conversa nunca
|
|
13
|
-
/// aparece em outra</b> — porque o estado por sessao vive na
|
|
14
|
-
/// <see cref="AgentSession"/> (via <see cref="ProviderSessionState{TState}"/>), jamais num
|
|
15
|
-
/// campo do provider.
|
|
16
|
-
/// </summary>
|
|
17
|
-
/// <remarks>
|
|
18
|
-
/// Os testes rodam pelo caminho <b>real</b> do MAF: um <see cref="ChatClientAgent"/> de
|
|
19
|
-
/// verdade, com <c>ChatClientAgentOptions.ChatHistoryProvider</c> apontando para o
|
|
20
|
-
/// <see cref="ConversationStore"/>, sessoes criadas por <c>CreateSessionAsync</c> e voltas
|
|
21
|
-
/// disparadas por <c>RunAsync</c>. O unico fake e o <see cref="FakeChatClient"/> no lugar do
|
|
22
|
-
/// provider de modelo — e por isso "o provider e chamado" e um fato observado, e nao uma
|
|
23
|
-
/// chamada manual ao metodo protegido fingindo ser o framework.
|
|
24
|
-
/// </remarks>
|
|
25
10
|
public sealed class ConversationStoreTests
|
|
26
11
|
{
|
|
27
12
|
private static ChatClientAgent Agent(FakeChatClient client, ConversationStore history)
|
|
@@ -37,15 +22,6 @@ public sealed class ConversationStoreTests
|
|
|
37
22
|
private static string[] TextsOf(IEnumerable<ChatMessage> messages)
|
|
38
23
|
=> [.. messages.Select(m => m.Text)];
|
|
39
24
|
|
|
40
|
-
// ---------------------------------------------------------------------------------
|
|
41
|
-
// doneCriteria 2: AppendAsync recebe a volta; LoadAsync alimenta a volta seguinte
|
|
42
|
-
// ---------------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/// <summary>
|
|
45
|
-
/// Uma volta e gravada com pedido e resposta, nessa ordem — e a volta seguinte chega ao
|
|
46
|
-
/// modelo ja com essa historia na frente. As duas metades do contrato, medidas nos dois
|
|
47
|
-
/// lados: no acervo e no que o cliente de modelo recebeu.
|
|
48
|
-
/// </summary>
|
|
49
25
|
[Fact]
|
|
50
26
|
public async Task TheRoundIsAppendedAndFeedsTheHistoryOfTheNextRound()
|
|
51
27
|
{
|
|
@@ -61,7 +37,6 @@ public sealed class ConversationStoreTests
|
|
|
61
37
|
|
|
62
38
|
await agent.RunAsync("Meu pedido 1234 esta atrasado.", session, cancellationToken: Ct);
|
|
63
39
|
|
|
64
|
-
// Depois da 1a volta o acervo tem exatamente o pedido e a resposta daquela volta.
|
|
65
40
|
var afterFirst = await store.LoadAsync("conversa-A", Ct);
|
|
66
41
|
Assert.Equal(
|
|
67
42
|
["Meu pedido 1234 esta atrasado.", "Vou verificar o pedido 1234."],
|
|
@@ -69,8 +44,6 @@ public sealed class ConversationStoreTests
|
|
|
69
44
|
|
|
70
45
|
await agent.RunAsync("E quando foi despachado?", session, cancellationToken: Ct);
|
|
71
46
|
|
|
72
|
-
// A 2a ida ao modelo levou o historico carregado do acervo, e so uma vez cada
|
|
73
|
-
// mensagem — o turno corrente nao vem duplicado.
|
|
74
47
|
var secondCall = TextsOf(client.ReceivedMessages[1]);
|
|
75
48
|
Assert.Equal(
|
|
76
49
|
[
|
|
@@ -84,10 +57,6 @@ public sealed class ConversationStoreTests
|
|
|
84
57
|
Assert.Equal(4, afterSecond.Count);
|
|
85
58
|
}
|
|
86
59
|
|
|
87
|
-
/// <summary>
|
|
88
|
-
/// A primeira volta nao carrega historico nenhum: conversa inexistente e lista vazia, nao
|
|
89
|
-
/// erro. E o caso mais comum de todos.
|
|
90
|
-
/// </summary>
|
|
91
60
|
[Fact]
|
|
92
61
|
public async Task AConversationThatDoesNotExistYetLoadsEmptyInsteadOfFailing()
|
|
93
62
|
{
|
|
@@ -106,19 +75,6 @@ public sealed class ConversationStoreTests
|
|
|
106
75
|
Assert.Equal(["oi", "ola"], TextsOf(await store.LoadAsync("conversa-nova", Ct)));
|
|
107
76
|
}
|
|
108
77
|
|
|
109
|
-
// ---------------------------------------------------------------------------------
|
|
110
|
-
// doneCriteria 1: DUAS sessoes concorrentes na MESMA instancia, sem vazamento
|
|
111
|
-
// ---------------------------------------------------------------------------------
|
|
112
|
-
|
|
113
|
-
/// <summary>
|
|
114
|
-
/// <b>O canario da invariante.</b> Uma instancia de <see cref="ConversationStore"/>, duas
|
|
115
|
-
/// sessoes, muitas voltas intercaladas em paralelo: nenhuma mensagem de uma conversa
|
|
116
|
-
/// aparece na outra, e cada uma tem exatamente as suas.
|
|
117
|
-
/// </summary>
|
|
118
|
-
/// <remarks>
|
|
119
|
-
/// Este e o teste que morre se alguem guardar "a conversa atual" num campo do provider —
|
|
120
|
-
/// o defeito que em producao aparece como historico de um usuario na tela de outro.
|
|
121
|
-
/// </remarks>
|
|
122
78
|
[Fact]
|
|
123
79
|
public async Task TwoConcurrentSessionsOnTheSameProviderNeverSeeEachOthersMessages()
|
|
124
80
|
{
|
|
@@ -136,7 +92,6 @@ public sealed class ConversationStoreTests
|
|
|
136
92
|
clientB.EnqueueText($"resposta-B-{i}");
|
|
137
93
|
}
|
|
138
94
|
|
|
139
|
-
// Duas conversas distintas, o MESMO provider de historico nos dois agentes.
|
|
140
95
|
var agentA = Agent(clientA, history);
|
|
141
96
|
var agentB = Agent(clientB, history);
|
|
142
97
|
|
|
@@ -145,7 +100,7 @@ public sealed class ConversationStoreTests
|
|
|
145
100
|
history.BindConversation(sessionA, "conversa-A");
|
|
146
101
|
history.BindConversation(sessionB, "conversa-B");
|
|
147
102
|
|
|
148
|
-
// Sequencial
|
|
103
|
+
// Sequencial dentro de cada sessao (uma conversa e uma fila), concorrente entre elas.
|
|
149
104
|
var driveA = Task.Run(async () =>
|
|
150
105
|
{
|
|
151
106
|
for (var i = 0; i < Rounds; i++)
|
|
@@ -170,26 +125,18 @@ public sealed class ConversationStoreTests
|
|
|
170
125
|
Assert.Equal(Rounds * 2, conversationA.Length);
|
|
171
126
|
Assert.Equal(Rounds * 2, conversationB.Length);
|
|
172
127
|
|
|
173
|
-
// Nenhum texto do outro lado atravessou.
|
|
174
128
|
Assert.DoesNotContain(conversationA, t => t.Contains("-B-", StringComparison.Ordinal));
|
|
175
129
|
Assert.DoesNotContain(conversationB, t => t.Contains("-A-", StringComparison.Ordinal));
|
|
176
130
|
|
|
177
|
-
// E cada conversa tem as suas, na ordem.
|
|
178
131
|
Assert.Equal(
|
|
179
132
|
[.. Enumerable.Range(0, Rounds).SelectMany(i => new[] { $"pergunta-A-{i}", $"resposta-A-{i}" })],
|
|
180
133
|
conversationA);
|
|
181
134
|
|
|
182
|
-
// O que o modelo VIU tambem nao se misturou: a ultima ida de A nao carrega nada de B.
|
|
183
135
|
Assert.DoesNotContain(
|
|
184
136
|
TextsOf(clientA.ReceivedMessages[^1]),
|
|
185
137
|
t => t.Contains("-B-", StringComparison.Ordinal));
|
|
186
138
|
}
|
|
187
139
|
|
|
188
|
-
/// <summary>
|
|
189
|
-
/// A prova direta de que o estado por sessao <b>mora na sessao</b>: o mesmo provider
|
|
190
|
-
/// devolve ids diferentes para sessoes diferentes, e o id de cada sessao esta gravado
|
|
191
|
-
/// no <c>StateBag</c> dela — que e onde o MAF o serializa e o retoma.
|
|
192
|
-
/// </summary>
|
|
193
140
|
[Fact]
|
|
194
141
|
public async Task TheConversationIdLivesInTheSessionStateNotInAProviderField()
|
|
195
142
|
{
|
|
@@ -204,19 +151,13 @@ public sealed class ConversationStoreTests
|
|
|
204
151
|
var secondId = history.GetConversationId(second);
|
|
205
152
|
|
|
206
153
|
Assert.NotEqual(firstId, secondId);
|
|
207
|
-
Assert.Equal(firstId, history.GetConversationId(first));
|
|
154
|
+
Assert.Equal(firstId, history.GetConversationId(first));
|
|
208
155
|
|
|
209
|
-
// Esta gravado na sessao, sob a chave que o provider declara em StateKeys.
|
|
210
156
|
Assert.True(first.StateBag.TryGetValue<string>(ConversationStore.ConversationIdStateKey, out var stored));
|
|
211
157
|
Assert.Equal(firstId, stored);
|
|
212
158
|
Assert.Contains(ConversationStore.ConversationIdStateKey, history.StateKeys);
|
|
213
159
|
}
|
|
214
160
|
|
|
215
|
-
/// <summary>
|
|
216
|
-
/// Sem amarracao explicita o id e um <see cref="Guid.CreateVersion7"/> — ordenavel no
|
|
217
|
-
/// tempo, ao contrario de <c>Guid.NewGuid</c>. A versao 7 e legivel no proprio valor
|
|
218
|
-
/// (o nibble de versao), entao a regra da casa e verificavel e nao so declarada.
|
|
219
|
-
/// </summary>
|
|
220
161
|
[Fact]
|
|
221
162
|
public async Task AnUnboundSessionGetsATimeOrderedIdentifier()
|
|
222
163
|
{
|
|
@@ -231,11 +172,6 @@ public sealed class ConversationStoreTests
|
|
|
231
172
|
Assert.Equal(7, parsed.ToByteArray(bigEndian: true)[6] >> 4);
|
|
232
173
|
}
|
|
233
174
|
|
|
234
|
-
/// <summary>
|
|
235
|
-
/// O caminho do CRM: a conversa ja existe como registro, e a sessao entra nela — o
|
|
236
|
-
/// historico anterior aparece na primeira volta desta sessao. E o que torna a decisao
|
|
237
|
-
/// "conversa no acervo do projeto" util em vez de teorica.
|
|
238
|
-
/// </summary>
|
|
239
175
|
[Fact]
|
|
240
176
|
public async Task ASessionCanJoinAConversationThatAlreadyExistsInTheProjectStore()
|
|
241
177
|
{
|
|
@@ -258,21 +194,10 @@ public sealed class ConversationStoreTests
|
|
|
258
194
|
await agent.RunAsync("Nao chegou.", session, cancellationToken: Ct);
|
|
259
195
|
|
|
260
196
|
Assert.Contains("Ontem eu pedi um orcamento.", TextsOf(client.ReceivedMessages[0]));
|
|
261
|
-
// 2 do acervo
|
|
262
|
-
// mesma: regravar o historico inteiro faria a conversa crescer em progressao
|
|
263
|
-
// geometrica a cada turno.
|
|
197
|
+
// 2 do acervo + pedido e resposta desta volta: a volta grava so a si mesma.
|
|
264
198
|
Assert.Equal(4, (await store.LoadAsync("crm-conversa-42", Ct)).Count);
|
|
265
199
|
}
|
|
266
200
|
|
|
267
|
-
// ---------------------------------------------------------------------------------
|
|
268
|
-
// O acervo e plugavel: o kit nao conhece banco
|
|
269
|
-
// ---------------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
/// <summary>
|
|
272
|
-
/// A costura e substituivel: um acervo de teste que apenas anota as chamadas prova que o
|
|
273
|
-
/// provider fala com <see cref="IConversationStore"/> e com mais nada — e um projeto que
|
|
274
|
-
/// troque memoria por <c>jsonb</c> so implementa esta interface.
|
|
275
|
-
/// </summary>
|
|
276
201
|
[Fact]
|
|
277
202
|
public async Task TheProviderTalksOnlyToTheSeamSoPersistenceIsReplaceable()
|
|
278
203
|
{
|
|
@@ -289,11 +214,6 @@ public sealed class ConversationStoreTests
|
|
|
289
214
|
Assert.Equal(["load:c-1", "append:c-1"], spy.Calls);
|
|
290
215
|
}
|
|
291
216
|
|
|
292
|
-
/// <summary>
|
|
293
|
-
/// Volta que falhou nao vira historico. Persistir o pedido sem resposta deixaria a
|
|
294
|
-
/// conversa terminando numa pergunta nunca respondida — e a volta seguinte a repetiria,
|
|
295
|
-
/// com o usuario se vendo duplicado.
|
|
296
|
-
/// </summary>
|
|
297
217
|
[Fact]
|
|
298
218
|
public async Task AFailedRoundIsNotPersisted()
|
|
299
219
|
{
|
|
@@ -311,22 +231,7 @@ public sealed class ConversationStoreTests
|
|
|
311
231
|
Assert.Empty(await store.LoadAsync("conversa-falha", Ct));
|
|
312
232
|
}
|
|
313
233
|
|
|
314
|
-
|
|
315
|
-
/// <b>Uma volta sem sessao.</b> O MAF declara <c>InvokingContext.Session</c> como
|
|
316
|
-
/// anulavel, e o provider recusa esse caso: inventar um id gravaria conversa orfa e usar
|
|
317
|
-
/// uma chave fixa juntaria tenants no mesmo balde — as duas saidas silenciosas sao
|
|
318
|
-
/// piores que a recusa. Este teste mede o que o framework realmente faz por esse
|
|
319
|
-
/// caminho, em vez de supor: e o comportamento observado que fica pinado aqui.
|
|
320
|
-
/// </summary>
|
|
321
|
-
/// <remarks>
|
|
322
|
-
/// O caminho de construir <c>InvokingContext</c> a mao — que seria a forma direta de
|
|
323
|
-
/// exercitar a guarda — <b>nao esta disponivel</b>: no MAF 1.20.0 os construtores de
|
|
324
|
-
/// <c>InvokingContext</c> e <c>InvokedContext</c> sao os unicos membros da area de
|
|
325
|
-
/// <c>ChatHistoryProvider</c> marcados <c>[Experimental("MAAI001")]</c>, e com
|
|
326
|
-
/// <c>TreatWarningsAsErrors</c> isso e erro de compilacao. Consequencia registrada: um
|
|
327
|
-
/// provider de historico so pode ser testado <b>atraves de um agente real</b>, que e
|
|
328
|
-
/// como esta classe inteira o testa.
|
|
329
|
-
/// </remarks>
|
|
234
|
+
// No MAF 1.20.0 o construtor de InvokingContext e [Experimental("MAAI001")]: o provider so se exercita via agente real.
|
|
330
235
|
[Fact]
|
|
331
236
|
public async Task ARoundWithoutAnExplicitSessionStillHasAConversationOfItsOwn()
|
|
332
237
|
{
|
|
@@ -337,14 +242,10 @@ public sealed class ConversationStoreTests
|
|
|
337
242
|
|
|
338
243
|
await agent.RunAsync("sem sessao", cancellationToken: Ct);
|
|
339
244
|
|
|
340
|
-
//
|
|
341
|
-
// alcancada por este caminho, e a conversa nasce com id proprio em vez de cair num
|
|
342
|
-
// balde compartilhado.
|
|
245
|
+
// Sem sessao, o agente cria uma efemera para a volta: a conversa nasce com id proprio.
|
|
343
246
|
var conversation = Assert.Single(store.ConversationIds);
|
|
344
247
|
Assert.Equal(["sem sessao", "respondido"], TextsOf(await store.LoadAsync(conversation, Ct)));
|
|
345
248
|
|
|
346
|
-
// E a volta seguinte, tambem sem sessao, NAO herda a conversa anterior — sem sessao
|
|
347
|
-
// nao ha continuidade, que e a semantica correta de uma chamada avulsa.
|
|
348
249
|
client.EnqueueText("de novo");
|
|
349
250
|
await agent.RunAsync("outra avulsa", cancellationToken: Ct);
|
|
350
251
|
|
|
@@ -352,15 +253,10 @@ public sealed class ConversationStoreTests
|
|
|
352
253
|
Assert.DoesNotContain("sem sessao", TextsOf(client.ReceivedMessages[^1]));
|
|
353
254
|
}
|
|
354
255
|
|
|
355
|
-
/// <summary>Provider sem acervo e defeito de composicao, e falha na construcao.</summary>
|
|
356
256
|
[Fact]
|
|
357
257
|
public void TheProviderRefusesToBeBuiltWithoutAStore()
|
|
358
258
|
=> Assert.Throws<ArgumentNullException>(() => new ConversationStore(null!));
|
|
359
259
|
|
|
360
|
-
/// <summary>
|
|
361
|
-
/// O acervo em memoria e ele proprio seguro sob concorrencia — senao o canario das duas
|
|
362
|
-
/// sessoes mediria a falta de trava aqui, e nao o isolamento do provider.
|
|
363
|
-
/// </summary>
|
|
364
260
|
[Fact]
|
|
365
261
|
public async Task TheInMemoryStoreDoesNotLoseWritesUnderConcurrency()
|
|
366
262
|
{
|
|
@@ -378,7 +274,6 @@ public sealed class ConversationStoreTests
|
|
|
378
274
|
Assert.Empty(await store.LoadAsync("inexistente", Ct));
|
|
379
275
|
}
|
|
380
276
|
|
|
381
|
-
/// <summary>Acervo de teste que so anota o que lhe pediram, na ordem.</summary>
|
|
382
277
|
private sealed class RecordingConversationStore : IConversationStore
|
|
383
278
|
{
|
|
384
279
|
private readonly ConcurrentQueue<string> _calls = new();
|