@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
|
@@ -5,17 +5,6 @@ using Xunit;
|
|
|
5
5
|
|
|
6
6
|
namespace Morph.AiKit.Tests.Evals;
|
|
7
7
|
|
|
8
|
-
/// <summary>
|
|
9
|
-
/// A prova que decide a feature: <b>com a cache quente, o eval roda sem credencial e sem
|
|
10
|
-
/// rede</b>.
|
|
11
|
-
/// </summary>
|
|
12
|
-
/// <remarks>
|
|
13
|
-
/// Essa frase foi derivada de leitura de fonte antes de existir codigo. Aqui ela vira
|
|
14
|
-
/// medicao: uma arvore de cache e gravada por um fake, o processo troca o cliente interno
|
|
15
|
-
/// por um que RECUSA qualquer chamada, e a suite termina verde. Se o cliente interno fosse
|
|
16
|
-
/// tocado uma unica vez, o contador dele deixaria de ser zero — e nenhuma das assercoes
|
|
17
|
-
/// abaixo passaria.
|
|
18
|
-
/// </remarks>
|
|
19
8
|
public sealed class EvalFixtureTests
|
|
20
9
|
{
|
|
21
10
|
[Fact]
|
|
@@ -25,18 +14,12 @@ public sealed class EvalFixtureTests
|
|
|
25
14
|
ws.WriteScenario("triagem", "acme", "saudacao", "bom dia", """[ { "kind": "contains", "value": "Ola" } ]""");
|
|
26
15
|
ws.WriteScenario("triagem", "acme", "handoff", "quero falar com humano", """[ { "kind": "contains", "value": "humano" } ]""");
|
|
27
16
|
|
|
28
|
-
// 1) GRAVA a arvore contra o fake — nunca contra uma API paga. O caminho de escrita
|
|
29
|
-
// exercitado e o da biblioteca, e a chave e computada pela mesma funcao que a
|
|
30
|
-
// leitura vai usar.
|
|
31
17
|
using var fake = new FakeChatClient
|
|
32
18
|
{
|
|
33
19
|
MetadataProviderName = "morph-fake",
|
|
34
20
|
MetadataDefaultModelId = "modelo-exemplar",
|
|
35
21
|
};
|
|
36
|
-
// A
|
|
37
|
-
// "saudacao". Enfileirar na ordem alfabetica errada foi como esta suite descobriu
|
|
38
|
-
// que LoadAll ordena — e a ordem estavel e o que faz duas maquinas gravarem a mesma
|
|
39
|
-
// arvore de cache.
|
|
22
|
+
// A ordem e a do dataset (ordinal por caminho): "handoff" antes de "saudacao".
|
|
40
23
|
fake.EnqueueText("Encaminhando para um humano agora.").EnqueueText("Ola! Bom dia.");
|
|
41
24
|
|
|
42
25
|
await using (var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake))
|
|
@@ -50,8 +33,6 @@ public sealed class EvalFixtureTests
|
|
|
50
33
|
Assert.Equal(2, fake.CallCount);
|
|
51
34
|
Assert.True(File.Exists(EvalCacheStamp.PathFor(ws.CacheRoot)), "o refresh tem de deixar o carimbo na raiz da cache");
|
|
52
35
|
|
|
53
|
-
// 2) LE offline. O cliente interno agora RECUSA: se a cache nao respondesse, este
|
|
54
|
-
// bloco lancaria EvalCacheMissException em vez de terminar verde.
|
|
55
36
|
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
56
37
|
var summary = await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
57
38
|
|
|
@@ -63,8 +44,6 @@ public sealed class EvalFixtureTests
|
|
|
63
44
|
Assert.Equal("ok", summary.Stamp);
|
|
64
45
|
Assert.Equal(0, offline.Offline!.Invocations);
|
|
65
46
|
|
|
66
|
-
// 3) O fake nao foi tocado de novo: a leitura offline nao passou por ele nem por
|
|
67
|
-
// acidente (ele esta noutro processo logico, mas a contagem prova a intencao).
|
|
68
47
|
Assert.Equal(2, fake.CallCount);
|
|
69
48
|
}
|
|
70
49
|
|
|
@@ -84,8 +63,6 @@ public sealed class EvalFixtureTests
|
|
|
84
63
|
await using var offline = EvalFixture.Create(ws.Options(EvalMode.Offline));
|
|
85
64
|
await offline.RunAllAsync(TestContext.Current.CancellationToken);
|
|
86
65
|
|
|
87
|
-
// Ler de volta o ARQUIVO, e nao o objeto em memoria: o contrato com o CLI e o
|
|
88
|
-
// arquivo, e um teste que so olhasse o objeto seria um espelho do gerador.
|
|
89
66
|
var reread = EvalRunSummary.ReadFrom(ws.SummaryPath);
|
|
90
67
|
Assert.NotNull(reread);
|
|
91
68
|
Assert.Equal(1, reread!.Scenarios);
|
|
@@ -103,17 +80,12 @@ public sealed class EvalFixtureTests
|
|
|
103
80
|
var error = Assert.Throws<ArgumentOutOfRangeException>(() => EvalFixture.Create(options));
|
|
104
81
|
|
|
105
82
|
Assert.Contains("eval-cache-ttl-too-short", error.Message, StringComparison.Ordinal);
|
|
106
|
-
// A mensagem tem de dizer POR QUE, e nao so que o numero e pequeno: a expiracao apaga
|
|
107
|
-
// o diretorio da chave durante a LEITURA.
|
|
108
83
|
Assert.Contains("DESTRUTIVA NA LEITURA", error.Message, StringComparison.Ordinal);
|
|
109
84
|
}
|
|
110
85
|
|
|
111
86
|
[Fact]
|
|
112
87
|
public void TheLibraryDefaultTimeToLiveWouldExpireACommittedCacheWithinWeeks()
|
|
113
88
|
{
|
|
114
|
-
// MEDIDO, nao lembrado: e este fato que torna o TTL explicito obrigatorio. Se um dia
|
|
115
|
-
// a biblioteca subir o default para alem do piso do kit, este teste morre e a
|
|
116
|
-
// exigencia pode ser rediscutida com um numero na mao.
|
|
117
89
|
Assert.True(
|
|
118
90
|
Defaults.DefaultTimeToLiveForCacheEntries < EvalCacheOptions.MinimumTimeToLive,
|
|
119
91
|
$"o default da biblioteca e {Defaults.DefaultTimeToLiveForCacheEntries.TotalDays:0} dias, "
|
|
@@ -6,20 +6,9 @@ using Xunit;
|
|
|
6
6
|
|
|
7
7
|
namespace Morph.AiKit.Tests.Evals;
|
|
8
8
|
|
|
9
|
-
/// <summary>
|
|
10
|
-
/// Regravacao, e o invariante barato que protege a premissa de commitabilidade:
|
|
11
|
-
/// <b>uma leitura nao escreve um byte na arvore de cache</b>.
|
|
12
|
-
/// </summary>
|
|
13
9
|
public sealed class EvalRefreshTests
|
|
14
10
|
{
|
|
15
|
-
|
|
16
|
-
/// Impressao digital da arvore: caminho relativo mais SHA-256 de cada arquivo.
|
|
17
|
-
/// </summary>
|
|
18
|
-
/// <remarks>
|
|
19
|
-
/// Comparar CONTEUDO, e nao data de modificacao: o que importa e se o proximo
|
|
20
|
-
/// <c>git status</c> mostra diff, e o git tambem olha conteudo. Um teste sobre mtime
|
|
21
|
-
/// falharia por gravacao identica e passaria por gravacao diferente no mesmo tick.
|
|
22
|
-
/// </remarks>
|
|
11
|
+
// Compara conteudo, nao mtime: e o conteudo que o git ve como diff.
|
|
23
12
|
private static IReadOnlyList<string> Fingerprint(string root)
|
|
24
13
|
{
|
|
25
14
|
if (!Directory.Exists(root))
|
|
@@ -63,9 +52,6 @@ public sealed class EvalRefreshTests
|
|
|
63
52
|
Assert.Equal(3, summary.Passed);
|
|
64
53
|
}
|
|
65
54
|
|
|
66
|
-
// Se algum dia a biblioteca passar a tocar disco no caminho de HIT, este canario morde
|
|
67
|
-
// no dia em que isso acontecer — e nao meses depois, quando alguem estranhar um diff
|
|
68
|
-
// que aparece sozinho em toda rodada.
|
|
69
55
|
Assert.Equal(before, Fingerprint(ws.CacheRoot));
|
|
70
56
|
}
|
|
71
57
|
|
|
@@ -79,9 +65,6 @@ public sealed class EvalRefreshTests
|
|
|
79
65
|
await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
|
|
80
66
|
await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
81
67
|
|
|
82
|
-
// Os resultados carregam latencia de cronometro, uso de tokens e o flag de cache hit:
|
|
83
|
-
// commita-los seria diff garantido em toda rodada, sem sinal nenhum. Por CONSTRUCAO
|
|
84
|
-
// (outra raiz), nunca por um .gitignore dentro de um diretorio commitado.
|
|
85
68
|
Assert.True(Directory.Exists(ws.ResultsRoot), "o store de resultados tem de existir depois de um run");
|
|
86
69
|
Assert.NotEmpty(Directory.EnumerateFiles(ws.ResultsRoot, "*", SearchOption.AllDirectories));
|
|
87
70
|
|
|
@@ -102,8 +85,6 @@ public sealed class EvalRefreshTests
|
|
|
102
85
|
await using var refresh = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake);
|
|
103
86
|
var summary = await refresh.RunAllAsync(TestContext.Current.CancellationToken);
|
|
104
87
|
|
|
105
|
-
// O contador do cliente real e o que torna "a cache esta fria" uma MEDICAO: a arvore
|
|
106
|
-
// mudou e precisa ser commitada, e isso e afirmacao com consequencia.
|
|
107
88
|
Assert.Equal(3, summary.Misses);
|
|
108
89
|
Assert.Equal(EvalCacheState.Cold, summary.Cache);
|
|
109
90
|
Assert.Equal(3, fake.CallCount);
|
|
@@ -121,9 +102,6 @@ public sealed class EvalRefreshTests
|
|
|
121
102
|
await first.RunAllAsync(TestContext.Current.CancellationToken);
|
|
122
103
|
}
|
|
123
104
|
|
|
124
|
-
// O cenario do meio some do dataset. Uma regravacao PARCIAL deixaria a entrada antiga
|
|
125
|
-
// dele viva na arvore, e a proxima leitura offline nao teria como saber que ela nao
|
|
126
|
-
// corresponde mais a nenhum cenario declarado.
|
|
127
105
|
File.Delete(Path.Combine(ws.DatasetRoot, "triagem", "acme", "b-segundo.json"));
|
|
128
106
|
|
|
129
107
|
using (var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" })
|
|
@@ -144,24 +122,14 @@ public sealed class EvalRefreshTests
|
|
|
144
122
|
using var ws = new EvalWorkspace();
|
|
145
123
|
using var fake = new FakeChatClient { MetadataProviderName = "morph-fake", MetadataDefaultModelId = "modelo-exemplar" };
|
|
146
124
|
|
|
147
|
-
// A lista vem do KIT, e nao do teste: e ela que da ao pacote .Quality cobertura de
|
|
148
|
-
// COMPILACAO em src/ (sem um using real la, o pin daquele pacote teria so contrato de
|
|
149
|
-
// versao, e um rename de API nao reprovaria PR nenhuma).
|
|
150
125
|
var quality = EvalQualityEvaluators.Stable();
|
|
151
126
|
|
|
152
127
|
await using var fixture = EvalFixture.Create(ws.Options(EvalMode.Refresh), fake, quality);
|
|
153
128
|
|
|
154
|
-
// A superficie existe e COMPILA contra as versoes do pin — que e o que um kit de
|
|
155
|
-
// template tem de garantir. O que ela NAO faz e decidir: o veredito de um cenario sai
|
|
156
|
-
// das assercoes deterministicas, e uma metrica de juizo entra no sumario como numero.
|
|
157
|
-
// Sem isso, a variancia de LLM reprovaria PRs e o no de eval nasceria destinado a ser
|
|
158
|
-
// desligado do CI.
|
|
159
129
|
Assert.Equal(2, quality.Count);
|
|
160
130
|
Assert.All(quality, evaluator => Assert.NotEmpty(evaluator.EvaluationMetricNames));
|
|
161
131
|
Assert.Empty(fixture.Summary.Quality);
|
|
162
132
|
|
|
163
|
-
// Uma lista NOVA a cada chamada: um array compartilhado seria estado global
|
|
164
|
-
// disfarcado de conveniencia, e a ReportingConfiguration guarda o que recebe.
|
|
165
133
|
Assert.NotSame(quality, EvalQualityEvaluators.Stable());
|
|
166
134
|
}
|
|
167
135
|
|
|
@@ -172,11 +140,7 @@ public sealed class EvalRefreshTests
|
|
|
172
140
|
[InlineData("Microsoft.Extensions.AI.Evaluation.Quality.ToolCallAccuracyEvaluator", true)]
|
|
173
141
|
public void WhichQualityEvaluatorsAreExperimentalIsMEASURED_NotRemembered(string typeName, bool experimental)
|
|
174
142
|
{
|
|
175
|
-
//
|
|
176
|
-
// [Experimental("AIEVAL001")] na 10.9.0, e usa-los sob TreatWarningsAsErrors exigiria
|
|
177
|
-
// suprimir um diagnostico — uma decisao consciente, nunca um efeito colateral. Se um
|
|
178
|
-
// dia a Microsoft estabilizar (ou marcar) algum deles, este teste morre e a escolha e
|
|
179
|
-
// rediscutida com o fato na mao, em vez de envelhecer em silencio num comentario.
|
|
143
|
+
// Dois dos quatro avaliadores sao [Experimental("AIEVAL001")] na 10.9.0 e exigiriam supressao sob TreatWarningsAsErrors.
|
|
180
144
|
var type = typeof(RelevanceEvaluator).Assembly.GetType(typeName);
|
|
181
145
|
Assert.NotNull(type);
|
|
182
146
|
|
|
@@ -2,15 +2,6 @@ using Morph.AiKit.Evals;
|
|
|
2
2
|
|
|
3
3
|
namespace Morph.AiKit.Tests.Evals;
|
|
4
4
|
|
|
5
|
-
/// <summary>
|
|
6
|
-
/// Um diretorio temporario com dataset, cache e resultados — a montagem que todo teste de
|
|
7
|
-
/// eval precisa, escrita uma vez.
|
|
8
|
-
/// </summary>
|
|
9
|
-
/// <remarks>
|
|
10
|
-
/// Ele existe para que os testes falem de COMPORTAMENTO e nao de caminhos. E cada instancia
|
|
11
|
-
/// tem raiz propria (GUID v7): dois testes rodando lado a lado nao podem disputar a mesma
|
|
12
|
-
/// arvore de cache, senao a falha aparece como flake e nao como defeito.
|
|
13
|
-
/// </remarks>
|
|
14
5
|
internal sealed class EvalWorkspace : IDisposable
|
|
15
6
|
{
|
|
16
7
|
public EvalWorkspace()
|
|
@@ -30,7 +21,6 @@ internal sealed class EvalWorkspace : IDisposable
|
|
|
30
21
|
|
|
31
22
|
public string SummaryPath => Path.Combine(Root, "summary.json");
|
|
32
23
|
|
|
33
|
-
/// <summary>O pin exemplar: a data e as versoes sob as quais a arvore diz ter sido gravada.</summary>
|
|
34
24
|
public static EvalPin Pin { get; } = new(
|
|
35
25
|
"2026-09-08",
|
|
36
26
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
|
@@ -39,7 +29,6 @@ internal sealed class EvalWorkspace : IDisposable
|
|
|
39
29
|
["Microsoft.Extensions.AI.Evaluation"] = "10.9.0",
|
|
40
30
|
});
|
|
41
31
|
|
|
42
|
-
/// <summary>Escreve um cenario no dataset e devolve o caminho do arquivo.</summary>
|
|
43
32
|
public string WriteScenario(string agent, string tenant, string name, string userText, string assertions)
|
|
44
33
|
{
|
|
45
34
|
var dir = Path.Combine(DatasetRoot, agent, tenant);
|
|
@@ -54,7 +43,6 @@ internal sealed class EvalWorkspace : IDisposable
|
|
|
54
43
|
return file;
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
/// <summary>Opcoes prontas para um modo, com TTL longo e o pin exemplar.</summary>
|
|
58
46
|
public EvalCacheOptions Options(EvalMode mode, EvalPin? pin = null) => new()
|
|
59
47
|
{
|
|
60
48
|
CacheRoot = CacheRoot,
|
|
@@ -79,8 +67,7 @@ internal sealed class EvalWorkspace : IDisposable
|
|
|
79
67
|
}
|
|
80
68
|
catch (IOException)
|
|
81
69
|
{
|
|
82
|
-
//
|
|
83
|
-
// que veio provar.
|
|
70
|
+
// Falha ao apagar o temporario nao deve derrubar um teste que ja provou o que veio provar.
|
|
84
71
|
}
|
|
85
72
|
}
|
|
86
73
|
}
|
|
@@ -4,42 +4,14 @@ using Xunit;
|
|
|
4
4
|
|
|
5
5
|
namespace Morph.AiKit.Tests.Evals;
|
|
6
6
|
|
|
7
|
-
/// <summary>
|
|
8
|
-
/// Os evals DO PROPRIO KIT, contra a arvore de cache COMMITADA — sem credencial, sem rede.
|
|
9
|
-
/// </summary>
|
|
10
|
-
/// <remarks>
|
|
11
|
-
/// <para>
|
|
12
|
-
/// Esta classe e o exemplar que um projeto copia. Ela roda com a categoria <c>Eval</c>, le a
|
|
13
|
-
/// cache commitada em <c>eval-cache/</c> e o dataset versionado em <c>eval-datasets/</c>, e
|
|
14
|
-
/// termina verde com TODAS as variaveis de credencial ausentes. Se algum cenario nao estiver
|
|
15
|
-
/// na cache, ela fica VERMELHA nomeando o cenario — nunca <c>skip</c>.
|
|
16
|
-
/// </para>
|
|
17
|
-
/// <para>
|
|
18
|
-
/// <b>A cache foi semeada por FAKE, jamais por chamada paga</b> (ver <see cref="SeedTheCommittedCacheTree"/>).
|
|
19
|
-
/// O caminho de ESCRITA exercitado e o da biblioteca e a chave e computada pela mesma funcao
|
|
20
|
-
/// que a leitura usa — entao a leitura offline le exatamente o que aquela escrita produziu.
|
|
21
|
-
/// A lacuna residual, declarada em voz alta: isto NAO prova que uma cache gravada contra um
|
|
22
|
-
/// provider real e legivel offline. O que a mitiga e um fato medido — a chave depende de
|
|
23
|
-
/// <c>ProviderName</c> e <c>DefaultModelId</c>, e nao do endpoint, e os dois estao sob nosso
|
|
24
|
-
/// controle no cliente offline.
|
|
25
|
-
/// </para>
|
|
26
|
-
/// </remarks>
|
|
27
7
|
public sealed class KitEvals
|
|
28
8
|
{
|
|
29
|
-
|
|
9
|
+
// Provider e Model entram na chave de cache: trocar qualquer um invalida a arvore commitada.
|
|
30
10
|
private const string Provider = "morph-fake";
|
|
31
11
|
|
|
32
|
-
/// <summary>Idem. Trocar qualquer um dos dois invalida a arvore inteira.</summary>
|
|
33
12
|
private const string Model = "morph-kit-exemplar";
|
|
34
13
|
|
|
35
|
-
|
|
36
|
-
/// O diretorio do PROJETO de teste, achado subindo a partir do assembly.
|
|
37
|
-
/// </summary>
|
|
38
|
-
/// <remarks>
|
|
39
|
-
/// De proposito: a alternativa seria copiar a cache para <c>bin/</c> e ler a copia — e ai
|
|
40
|
-
/// o canario de "um run offline nao suja a arvore commitada" seria verdadeiro por
|
|
41
|
-
/// vacuidade, porque a arvore commitada nem teria sido tocada.
|
|
42
|
-
/// </remarks>
|
|
14
|
+
// Le a arvore commitada no projeto, nao uma copia em bin/: senao o canario de "nao suja a arvore" seria vacuo.
|
|
43
15
|
private static string ProjectDirectory
|
|
44
16
|
{
|
|
45
17
|
get
|
|
@@ -59,7 +31,6 @@ public sealed class KitEvals
|
|
|
59
31
|
|
|
60
32
|
private static string DatasetRoot => Path.Combine(ProjectDirectory, "eval-datasets");
|
|
61
33
|
|
|
62
|
-
/// <summary>O pin sob o qual a arvore commitada foi gravada — o mesmo que o carimbo carrega.</summary>
|
|
63
34
|
private static EvalPin Pin => new(
|
|
64
35
|
"2026-09-08",
|
|
65
36
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
|
@@ -74,8 +45,6 @@ public sealed class KitEvals
|
|
|
74
45
|
{
|
|
75
46
|
CacheRoot = CacheRoot,
|
|
76
47
|
DatasetRoot = DatasetRoot,
|
|
77
|
-
// FORA da arvore commitada: os resultados carregam latencia, uso de tokens e o flag
|
|
78
|
-
// de cache hit, e commita-los seria diff garantido em toda rodada.
|
|
79
48
|
ResultsRoot = Path.Combine(Path.GetTempPath(), "morph-kit-eval-results"),
|
|
80
49
|
TimeToLive = TimeSpan.FromDays(3650),
|
|
81
50
|
ProviderName = Provider,
|
|
@@ -89,9 +58,6 @@ public sealed class KitEvals
|
|
|
89
58
|
[Trait("Category", "Eval")]
|
|
90
59
|
public async Task TheKitEvalsRunGreenFromTheCommittedCacheWithNoCredentialAtAll()
|
|
91
60
|
{
|
|
92
|
-
// Nenhuma variavel de credencial e lida por nada nesta montagem — o cliente interno
|
|
93
|
-
// e o OfflineChatClient, que nao abre socket e nao le ambiente. Se a cache nao
|
|
94
|
-
// respondesse, este teste morreria com EvalCacheMissException nomeando o cenario.
|
|
95
61
|
var summaryPath = Path.Combine(Path.GetTempPath(), $"morph-kit-eval-{Guid.CreateVersion7():N}.json");
|
|
96
62
|
|
|
97
63
|
try
|
|
@@ -102,20 +68,13 @@ public sealed class KitEvals
|
|
|
102
68
|
Assert.Equal(EvalCacheState.Warm, summary.Cache);
|
|
103
69
|
Assert.Equal("ok", summary.Stamp);
|
|
104
70
|
|
|
105
|
-
// NOMEANDO. `Assert.Equal(0, summary.Failed)` produzia "Expected 0, Actual 1" —
|
|
106
|
-
// verdadeiro e inutil: nao dizia qual cenario nem qual regua. A rubrica
|
|
107
|
-
// eval-de-modelo.md exige, na banda 9-10, derrubar o cenario NOMEANDO-O.
|
|
108
71
|
Assert.True(summary.Failed == 0, summary.DescribeFailures());
|
|
109
72
|
Assert.Empty(summary.Failures);
|
|
110
73
|
Assert.Equal(summary.Scenarios, summary.Passed);
|
|
111
74
|
|
|
112
|
-
// A prova central, e ela e um CONTADOR: o cliente interno e o unico caminho para
|
|
113
|
-
// a rede, e ele nao foi invocado nenhuma vez.
|
|
114
75
|
Assert.Equal(0, summary.Misses);
|
|
115
76
|
Assert.Equal(0, fixture.Offline!.Invocations);
|
|
116
77
|
|
|
117
|
-
// O dataset nao pode encolher em silencio: uma suite de eval que passa a avaliar
|
|
118
|
-
// menos cenarios e a mesma classe de defeito que uma baseline de teste caindo.
|
|
119
78
|
Assert.Equal(4, summary.Scenarios);
|
|
120
79
|
}
|
|
121
80
|
finally
|
|
@@ -144,17 +103,10 @@ public sealed class KitEvals
|
|
|
144
103
|
[Trait("Category", "Eval")]
|
|
145
104
|
public void TheCommittedCacheCarriesNoABSOLUTEPathOfTheMachineThatWroteIt()
|
|
146
105
|
{
|
|
147
|
-
// A premissa que torna a arvore commitavel: nada dentro dela aponta para o disco de
|
|
148
|
-
// quem gravou. Medido, e nao suposto — se um dia a biblioteca passar a gravar o
|
|
149
|
-
// storageRootPath dentro das entradas, este canario morde.
|
|
150
106
|
var root = ProjectDirectory.Replace('\\', '/');
|
|
151
107
|
var drive = Path.GetPathRoot(ProjectDirectory)?.Replace('\\', '/');
|
|
152
108
|
|
|
153
|
-
//
|
|
154
|
-
// `GetPathRoot` devolve "/", que nao identifica nada — e exigir que a cache nao
|
|
155
|
-
// contenha "/" reprova qualquer arquivo com um path ou uma URL dentro. O canario
|
|
156
|
-
// passava no Windows e era ABSURDO no Linux: media "nenhuma barra em lugar nenhum".
|
|
157
|
-
// Medido no CI (job ai-kit, ubuntu): 1 falha, `Assert.DoesNotContain() Sub-string found`.
|
|
109
|
+
// Em POSIX GetPathRoot devolve "/", que nao identifica maquina; so unidade nomeada ("R:/") identifica.
|
|
158
110
|
var namedDrive = drive is { Length: > 1 } && drive.Contains(':');
|
|
159
111
|
|
|
160
112
|
foreach (var file in Directory.EnumerateFiles(CacheRoot, "*", SearchOption.AllDirectories))
|
|
@@ -168,21 +120,6 @@ public sealed class KitEvals
|
|
|
168
120
|
}
|
|
169
121
|
}
|
|
170
122
|
|
|
171
|
-
/// <summary>
|
|
172
|
-
/// Regrava a arvore commitada a partir do fake. NAO roda em CI.
|
|
173
|
-
/// </summary>
|
|
174
|
-
/// <remarks>
|
|
175
|
-
/// <para>
|
|
176
|
-
/// Ele existe committed, e nao como um script que alguem rodou uma vez e jogou fora,
|
|
177
|
-
/// porque a proxima pessoa que subir o pin precisa saber COMO regravar — e uma instrucao
|
|
178
|
-
/// em prosa envelhece; um teste executavel nao.
|
|
179
|
-
/// </para>
|
|
180
|
-
/// <para>
|
|
181
|
-
/// A porta e uma variavel de ambiente, e nao um <c>[Fact(Skip=...)]</c> fixo, porque um
|
|
182
|
-
/// skip fixo nunca poderia ser exercitado: seria um seeder que ninguem consegue rodar.
|
|
183
|
-
/// Rode com <c>MORPH_EVAL_SEED=1 dotnet test</c> e commite a arvore que sair.
|
|
184
|
-
/// </para>
|
|
185
|
-
/// </remarks>
|
|
186
123
|
[Fact]
|
|
187
124
|
public async Task SeedTheCommittedCacheTree()
|
|
188
125
|
{
|
|
@@ -196,9 +133,7 @@ public sealed class KitEvals
|
|
|
196
133
|
MetadataDefaultModelId = Model,
|
|
197
134
|
};
|
|
198
135
|
|
|
199
|
-
// Uma resposta por cenario, na
|
|
200
|
-
// orcamento/acme/a-resposta-estruturada, triagem/acme/a-saudacao,
|
|
201
|
-
// triagem/acme/b-escalonamento, triagem/globex/a-saudacao.
|
|
136
|
+
// Uma resposta por cenario, na ordem ordinal de caminho do dataset.
|
|
202
137
|
fake.EnqueueText("""{"valor":"R$ 1.200,00","prazo":"5 dias uteis"}""")
|
|
203
138
|
.EnqueueText("Bom dia! Como posso ajudar voce hoje?")
|
|
204
139
|
.EnqueueText("Encaminhando voce para um atendente humano agora.")
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs
CHANGED
|
@@ -4,9 +4,6 @@ using Xunit;
|
|
|
4
4
|
|
|
5
5
|
namespace Morph.AiKit.Tests.Evals;
|
|
6
6
|
|
|
7
|
-
/// <summary>
|
|
8
|
-
/// O cliente que RECUSA: a peca de que depende "o eval roda no CI sem credencial".
|
|
9
|
-
/// </summary>
|
|
10
7
|
public sealed class OfflineChatClientTests
|
|
11
8
|
{
|
|
12
9
|
private static OfflineChatClient Build(Func<string>? scenario = null)
|
|
@@ -19,13 +16,9 @@ public sealed class OfflineChatClientTests
|
|
|
19
16
|
|
|
20
17
|
var metadata = Assert.IsType<ChatClientMetadata>(client.GetService(typeof(ChatClientMetadata)));
|
|
21
18
|
|
|
22
|
-
// Estes DOIS campos entram na chave de cache. Se divergissem do run que gravou, todo
|
|
23
|
-
// cenario daria miss e a cache quente seria indistinguivel de uma cache vazia.
|
|
24
19
|
Assert.Equal("morph-fake", metadata.ProviderName);
|
|
25
20
|
Assert.Equal("modelo-exemplar", metadata.DefaultModelId);
|
|
26
21
|
|
|
27
|
-
// E o endpoint e NULO de proposito: ele nao entra na chave (medido na v10.9.0), e um
|
|
28
|
-
// cliente que nunca liga para lugar nenhum nao deve anunciar um destino.
|
|
29
22
|
Assert.Null(metadata.ProviderUri);
|
|
30
23
|
}
|
|
31
24
|
|
|
@@ -59,9 +52,7 @@ public sealed class OfflineChatClientTests
|
|
|
59
52
|
[Fact]
|
|
60
53
|
public void TheScenarioNameIsReadAtCALLTIME_NotCaptured()
|
|
61
54
|
{
|
|
62
|
-
//
|
|
63
|
-
// construcao, toda mensagem de miss nomearia o primeiro cenario e mandaria o leitor
|
|
64
|
-
// investigar o arquivo errado.
|
|
55
|
+
// O fixture roda N cenarios com o mesmo cliente: o nome do cenario nao pode ser capturado na construcao.
|
|
65
56
|
var current = "primeiro";
|
|
66
57
|
using var client = Build(() => current);
|
|
67
58
|
|
|
@@ -3,30 +3,6 @@ using Microsoft.Extensions.AI;
|
|
|
3
3
|
|
|
4
4
|
namespace Morph.AiKit.Tests.Fakes;
|
|
5
5
|
|
|
6
|
-
/// <summary>
|
|
7
|
-
/// <see cref="IChatClient"/> roteirizado: a costura que dispensa rede e chave de API na
|
|
8
|
-
/// suite inteira do kit.
|
|
9
|
-
/// </summary>
|
|
10
|
-
/// <remarks>
|
|
11
|
-
/// <para>
|
|
12
|
-
/// <b>Por que ele e o coracao da historia de teste:</b> o unico ponto em que o kit toca um
|
|
13
|
-
/// provider e um <see cref="IChatClient"/>. Trocando esse ponto por uma fila de respostas
|
|
14
|
-
/// escritas a mao, TODO o resto — <c>ModelRegistry</c>, <c>AgentFactory</c>,
|
|
15
|
-
/// <c>UsageCostMiddleware</c>, <c>ConversationStore</c> — passa a ser testavel sem socket,
|
|
16
|
-
/// sem segredo e sem flake. Nenhum tipo desta pasta abre cliente HTTP nem le variavel de
|
|
17
|
-
/// ambiente — o canario estrutural disso e um grep sobre <c>tests/</c> (chave de provider,
|
|
18
|
-
/// leitura de ambiente, cliente HTTP) que precisa voltar VAZIO, e o canario de
|
|
19
|
-
/// comportamento e
|
|
20
|
-
/// <c>FakeChatClientTests.TheWholeSeamAnswersFromMemoryAloneWithNoConfigurationAndNoConnection</c>.
|
|
21
|
-
/// </para>
|
|
22
|
-
/// <para>
|
|
23
|
-
/// <b>Chamada nao roteirizada e defeito de teste, nao "comportamento padrao".</b> Esgotada
|
|
24
|
-
/// a fila, o fake lanca com a contagem exata de chamadas. Devolver uma resposta vazia
|
|
25
|
-
/// silenciosamente faria um teste passar por engano — que e a unica coisa pior do que um
|
|
26
|
-
/// teste falhando.
|
|
27
|
-
/// </para>
|
|
28
|
-
/// <para>Seguro para uso concorrente: fila, contadores e capturas vivem sob um lock unico.</para>
|
|
29
|
-
/// </remarks>
|
|
30
6
|
public sealed class FakeChatClient : IChatClient
|
|
31
7
|
{
|
|
32
8
|
private readonly Lock _gate = new();
|
|
@@ -34,12 +10,10 @@ public sealed class FakeChatClient : IChatClient
|
|
|
34
10
|
private readonly List<ChatOptions?> _receivedOptions = [];
|
|
35
11
|
private readonly List<IReadOnlyList<ChatMessage>> _receivedMessages = [];
|
|
36
12
|
|
|
37
|
-
/// <summary>Cria um fake com a fila vazia. Enfileire antes de usar.</summary>
|
|
38
13
|
public FakeChatClient()
|
|
39
14
|
{
|
|
40
15
|
}
|
|
41
16
|
|
|
42
|
-
/// <summary>Cria um fake ja roteirizado com as respostas dadas, na ordem.</summary>
|
|
43
17
|
public FakeChatClient(params ChatResponse[] scripted)
|
|
44
18
|
{
|
|
45
19
|
ArgumentNullException.ThrowIfNull(scripted);
|
|
@@ -50,7 +24,6 @@ public sealed class FakeChatClient : IChatClient
|
|
|
50
24
|
}
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
/// <summary>Atalho: uma resposta de assistente por texto, na ordem dada.</summary>
|
|
54
27
|
public static FakeChatClient Scripted(params string[] texts)
|
|
55
28
|
{
|
|
56
29
|
ArgumentNullException.ThrowIfNull(texts);
|
|
@@ -64,51 +37,35 @@ public sealed class FakeChatClient : IChatClient
|
|
|
64
37
|
return client;
|
|
65
38
|
}
|
|
66
39
|
|
|
67
|
-
/// <summary>Quantas vezes o cliente foi invocado (streaming incluso).</summary>
|
|
68
40
|
public int CallCount { get; private set; }
|
|
69
41
|
|
|
70
|
-
|
|
71
|
-
/// O <c>ProviderName</c> anunciado no <see cref="ChatClientMetadata"/>. Configuravel
|
|
72
|
-
/// porque ele ENTRA NA CHAVE da cache de eval: a arvore que este fake grava so e legivel
|
|
73
|
-
/// depois pelo <c>OfflineChatClient</c> se os dois se apresentarem com a mesma
|
|
74
|
-
/// identidade. Default preservado ("fake"): quem nao usa eval nao muda de comportamento.
|
|
75
|
-
/// </summary>
|
|
42
|
+
// ProviderName e DefaultModelId entram na chave da cache de eval: o OfflineChatClient precisa anunciar os mesmos.
|
|
76
43
|
public string MetadataProviderName { get; init; } = "fake";
|
|
77
44
|
|
|
78
|
-
/// <summary>
|
|
79
|
-
/// O <c>DefaultModelId</c> anunciado no <see cref="ChatClientMetadata"/>. Tambem entra na
|
|
80
|
-
/// chave de cache. Nulo por default, como antes.
|
|
81
|
-
/// </summary>
|
|
82
45
|
public string? MetadataDefaultModelId { get; init; }
|
|
83
46
|
|
|
84
|
-
/// <summary>Se <see cref="Dispose"/> ja foi chamado — usado para provar ownership de cache.</summary>
|
|
85
47
|
public bool IsDisposed { get; private set; }
|
|
86
48
|
|
|
87
|
-
/// <summary>As <see cref="ChatOptions"/> recebidas, uma entrada por chamada, na ordem.</summary>
|
|
88
49
|
public IReadOnlyList<ChatOptions?> ReceivedOptions
|
|
89
50
|
{
|
|
90
51
|
get { lock (_gate) { return [.. _receivedOptions]; } }
|
|
91
52
|
}
|
|
92
53
|
|
|
93
|
-
/// <summary>As mensagens recebidas, uma entrada por chamada, na ordem.</summary>
|
|
94
54
|
public IReadOnlyList<IReadOnlyList<ChatMessage>> ReceivedMessages
|
|
95
55
|
{
|
|
96
56
|
get { lock (_gate) { return [.. _receivedMessages]; } }
|
|
97
57
|
}
|
|
98
58
|
|
|
99
|
-
/// <summary>As opcoes da ultima chamada, ou nulo se nunca foi chamado.</summary>
|
|
100
59
|
public ChatOptions? LastOptions
|
|
101
60
|
{
|
|
102
61
|
get { lock (_gate) { return _receivedOptions.Count == 0 ? null : _receivedOptions[^1]; } }
|
|
103
62
|
}
|
|
104
63
|
|
|
105
|
-
/// <summary>As mensagens da ultima chamada, ou nulo se nunca foi chamado.</summary>
|
|
106
64
|
public IReadOnlyList<ChatMessage>? LastMessages
|
|
107
65
|
{
|
|
108
66
|
get { lock (_gate) { return _receivedMessages.Count == 0 ? null : _receivedMessages[^1]; } }
|
|
109
67
|
}
|
|
110
68
|
|
|
111
|
-
/// <summary>Enfileira uma resposta ja montada. Devolve o proprio fake, para encadear.</summary>
|
|
112
69
|
public FakeChatClient Enqueue(ChatResponse response)
|
|
113
70
|
{
|
|
114
71
|
ArgumentNullException.ThrowIfNull(response);
|
|
@@ -121,18 +78,9 @@ public sealed class FakeChatClient : IChatClient
|
|
|
121
78
|
return this;
|
|
122
79
|
}
|
|
123
80
|
|
|
124
|
-
/// <summary>
|
|
125
|
-
/// Enfileira uma resposta de assistente com o texto dado e, opcionalmente, um
|
|
126
|
-
/// <see cref="UsageDetails"/> controlado — e assim que o teste do
|
|
127
|
-
/// <c>UsageCostMiddleware</c> (T11) verifica tarifa sem rede.
|
|
128
|
-
/// </summary>
|
|
129
81
|
public FakeChatClient EnqueueText(string text, UsageDetails? usage = null)
|
|
130
82
|
=> Enqueue(new ChatResponse(new ChatMessage(ChatRole.Assistant, text)) { Usage = usage });
|
|
131
83
|
|
|
132
|
-
/// <summary>
|
|
133
|
-
/// Enfileira uma resposta com contagem de tokens conhecida. Atalho para o caso mais
|
|
134
|
-
/// comum de teste de custo.
|
|
135
|
-
/// </summary>
|
|
136
84
|
public FakeChatClient EnqueueTextWithUsage(
|
|
137
85
|
string text,
|
|
138
86
|
long inputTokens,
|
|
@@ -148,7 +96,6 @@ public sealed class FakeChatClient : IChatClient
|
|
|
148
96
|
CachedInputTokenCount = cachedInputTokens,
|
|
149
97
|
});
|
|
150
98
|
|
|
151
|
-
/// <inheritdoc />
|
|
152
99
|
public Task<ChatResponse> GetResponseAsync(
|
|
153
100
|
IEnumerable<ChatMessage> messages,
|
|
154
101
|
ChatOptions? options = null,
|
|
@@ -160,7 +107,6 @@ public sealed class FakeChatClient : IChatClient
|
|
|
160
107
|
return Task.FromResult(Record(messages, options));
|
|
161
108
|
}
|
|
162
109
|
|
|
163
|
-
/// <inheritdoc />
|
|
164
110
|
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
|
165
111
|
IEnumerable<ChatMessage> messages,
|
|
166
112
|
ChatOptions? options = null,
|
|
@@ -180,7 +126,6 @@ public sealed class FakeChatClient : IChatClient
|
|
|
180
126
|
await Task.CompletedTask.ConfigureAwait(false);
|
|
181
127
|
}
|
|
182
128
|
|
|
183
|
-
/// <inheritdoc />
|
|
184
129
|
public object? GetService(Type serviceType, object? serviceKey = null)
|
|
185
130
|
{
|
|
186
131
|
ArgumentNullException.ThrowIfNull(serviceType);
|
|
@@ -198,7 +143,6 @@ public sealed class FakeChatClient : IChatClient
|
|
|
198
143
|
return serviceType.IsInstanceOfType(this) ? this : null;
|
|
199
144
|
}
|
|
200
145
|
|
|
201
|
-
/// <inheritdoc />
|
|
202
146
|
public void Dispose() => IsDisposed = true;
|
|
203
147
|
|
|
204
148
|
private ChatResponse Record(IEnumerable<ChatMessage> messages, ChatOptions? options)
|
package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs
CHANGED
|
@@ -4,17 +4,6 @@ using Xunit;
|
|
|
4
4
|
|
|
5
5
|
namespace Morph.AiKit.Tests.Fakes;
|
|
6
6
|
|
|
7
|
-
/// <summary>
|
|
8
|
-
/// Canarios da costura de teste (T9). Um fake so vale se ele proprio for testado: um fake
|
|
9
|
-
/// que devolve na ordem errada, ou que engole as <see cref="ChatOptions"/> recebidas,
|
|
10
|
-
/// transforma todo teste que depende dele num teste que nao mede nada.
|
|
11
|
-
/// </summary>
|
|
12
|
-
/// <remarks>
|
|
13
|
-
/// Todo <c>await</c> passa <c>TestContext.Current.CancellationToken</c> porque o analisador
|
|
14
|
-
/// xUnit1051 cobra isso e <c>TreatWarningsAsErrors</c> transforma a cobranca em build
|
|
15
|
-
/// vermelho. Nao ha <c>#pragma</c> aqui: a regra e boa (teste cancelavel) e o custo e um
|
|
16
|
-
/// argumento.
|
|
17
|
-
/// </remarks>
|
|
18
7
|
public sealed class FakeChatClientTests
|
|
19
8
|
{
|
|
20
9
|
[Fact]
|
|
@@ -114,8 +103,6 @@ public sealed class FakeChatClientTests
|
|
|
114
103
|
[Fact]
|
|
115
104
|
public async Task UsageIsControlledByTheTestAndSurvivesTheRoundTrip()
|
|
116
105
|
{
|
|
117
|
-
// E assim que a T11 vai verificar tarifa sem rede: Usage conhecido entra,
|
|
118
|
-
// Usage conhecido sai.
|
|
119
106
|
using var client = new FakeChatClient();
|
|
120
107
|
client.EnqueueTextWithUsage("ok", inputTokens: 1_000, outputTokens: 250, cachedInputTokens: 400);
|
|
121
108
|
|
|
@@ -155,17 +142,6 @@ public sealed class FakeChatClientTests
|
|
|
155
142
|
[Fact]
|
|
156
143
|
public async Task TheWholeSeamAnswersFromMemoryAloneWithNoConfigurationAndNoConnection()
|
|
157
144
|
{
|
|
158
|
-
// O caminho inteiro que os consumidores usam — documento JSON, registry, fabrica,
|
|
159
|
-
// cliente — exercitado com nada alem de objetos em memoria: nenhuma chave, nenhum
|
|
160
|
-
// arquivo, nenhum socket. E este o teste a apontar quando alguem perguntar "como o
|
|
161
|
-
// kit testa IA sem provider".
|
|
162
|
-
//
|
|
163
|
-
// A propriedade "passa sem chave de provider no ambiente" NAO e afirmada aqui por
|
|
164
|
-
// codigo: afirma-la exigiria ler variavel de ambiente, e um teste que faz isso
|
|
165
|
-
// derruba o canario estrutural da T9 — o grep sobre tests/ (chave de provider,
|
|
166
|
-
// leitura de ambiente, cliente HTTP) tem de voltar VAZIO. A propriedade e
|
|
167
|
-
// DEMONSTRADA rodando a suite com o ambiente esvaziado, o que cobre os 50 testes
|
|
168
|
-
// em vez de um.
|
|
169
145
|
var document = """
|
|
170
146
|
{
|
|
171
147
|
"version": "2.0",
|
|
@@ -195,8 +171,6 @@ public sealed class FakeChatClientTests
|
|
|
195
171
|
[Fact]
|
|
196
172
|
public async Task ConcurrentCallersNeverLoseACallOrACapture()
|
|
197
173
|
{
|
|
198
|
-
// ConversationStore (T13) exercita duas sessoes sobre a MESMA instancia. Se o fake
|
|
199
|
-
// perdesse chamada sob concorrencia, aquele teste mediria o fake, nao o store.
|
|
200
174
|
using var client = new FakeChatClient();
|
|
201
175
|
for (var i = 0; i < 50; i++)
|
|
202
176
|
{
|
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
namespace Morph.AiKit.Tests.Fakes;
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
/// <see cref="TimeProvider"/> controlado pelo teste: o relogio so anda quando o teste manda.
|
|
5
|
-
/// </summary>
|
|
6
|
-
/// <remarks>
|
|
7
|
-
/// <para>
|
|
8
|
-
/// Existe <b>escrito a mao, em 20 linhas</b>, e nao via
|
|
9
|
-
/// <c>Microsoft.Extensions.TimeProvider.Testing</c>, por uma razao de contrato desta
|
|
10
|
-
/// feature: toda versao de pacote vem de <c>framework/ai-pin.json</c>, e aquele pacote nao
|
|
11
|
-
/// esta no pin. Acrescenta-lo seria uma versao decidida fora do pin — exatamente a segunda
|
|
12
|
-
/// verdade que o kit existe para impedir. O achado esta registrado no relatorio da T11.
|
|
13
|
-
/// </para>
|
|
14
|
-
/// <para>
|
|
15
|
-
/// So o necessario esta implementado: <see cref="GetUtcNow"/> e o avanco manual. Timers nao
|
|
16
|
-
/// sao usados por nenhum tipo do kit; herdar o comportamento base e mais honesto que
|
|
17
|
-
/// simular um agendador que ninguem exercita.
|
|
18
|
-
/// </para>
|
|
19
|
-
/// </remarks>
|
|
3
|
+
// Escrito a mao: Microsoft.Extensions.TimeProvider.Testing nao esta no pin (framework/ai-pin.json).
|
|
20
4
|
public sealed class FakeClock(DateTimeOffset start) : TimeProvider
|
|
21
5
|
{
|
|
22
6
|
private DateTimeOffset _now = start;
|
|
23
7
|
|
|
24
|
-
/// <inheritdoc />
|
|
25
8
|
public override DateTimeOffset GetUtcNow() => _now;
|
|
26
9
|
|
|
27
|
-
/// <summary>Avanca o relogio. Rejeita andar para tras: o tempo do teste e monotono.</summary>
|
|
28
10
|
public void Advance(TimeSpan delta)
|
|
29
11
|
{
|
|
30
12
|
ArgumentOutOfRangeException.ThrowIfLessThan(delta, TimeSpan.Zero);
|