@polymorphism-tech/morph-spec 8.33.0 → 8.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +28 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Pure orchestration — reuses (never reimplements):
|
|
15
15
|
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
16
|
-
* - runTestSuite()
|
|
16
|
+
* - runTestSuite() from src/lib/tasks/test-runner.js (one node)
|
|
17
|
+
* - resolveTestNodes() from src/lib/tasks/test-nodes.js (the LIST of suites)
|
|
17
18
|
* - runValidation() from ./validation-runner.js
|
|
18
19
|
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
19
20
|
* - loadRubric() + aggregateScores() from ./score-aggregator.js
|
|
@@ -28,7 +29,16 @@ import { join, resolve } from 'path';
|
|
|
28
29
|
import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
29
30
|
import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
30
31
|
import { buildComposeEnv } from '../e2e/compose-env.js';
|
|
31
|
-
import { runTestSuite,
|
|
32
|
+
import { runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout } from '../tasks/test-runner.js';
|
|
33
|
+
import {
|
|
34
|
+
resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
|
|
35
|
+
} from '../tasks/test-nodes.js';
|
|
36
|
+
import { runLinuxSuite, checkLinuxPreconditions } from '../tasks/linux-runner.js';
|
|
37
|
+
import {
|
|
38
|
+
EVAL_RUNNER, evalPreflight, evalNodeEnv, readEvalSummary, evalScenarioCount,
|
|
39
|
+
evalFailureLines,
|
|
40
|
+
evalEmptyRunProblem, evalColdCacheWarning,
|
|
41
|
+
} from '../tasks/eval-node.js';
|
|
32
42
|
import { runValidation } from './validation-runner.js';
|
|
33
43
|
import { loadRubric, aggregateScores } from './score-aggregator.js';
|
|
34
44
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
@@ -40,6 +50,23 @@ const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
|
40
50
|
/** Number of trailing output lines kept for feedback/reporting. */
|
|
41
51
|
const OUTPUT_TAIL_LINES = 20;
|
|
42
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The caller asked for something impossible — not a red verification, a wrong
|
|
55
|
+
* invocation. The distinction is the exit code: `verify` answers 1 for "the
|
|
56
|
+
* code is not ready" and 2 for "this command cannot mean what you typed".
|
|
57
|
+
*
|
|
58
|
+
* It exists because `--filter` can select nothing. Running every suite (or,
|
|
59
|
+
* far worse, none of them) and reporting green would be F30 restated as a
|
|
60
|
+
* one-word typo — so the runner refuses instead of guessing.
|
|
61
|
+
*/
|
|
62
|
+
export class VerifyUsageError extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = 'VerifyUsageError';
|
|
66
|
+
this.code = 'verify-usage';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
/**
|
|
44
71
|
* Run the deterministic verify cadence for a feature (or a single task within it).
|
|
45
72
|
*
|
|
@@ -54,6 +81,11 @@ const OUTPUT_TAIL_LINES = 20;
|
|
|
54
81
|
* @param {boolean} [options.skipBuild] - Skip the build node
|
|
55
82
|
* @param {boolean} [options.skipE2e] - Skip the e2e node
|
|
56
83
|
* @param {boolean} [options.freshE2e] - Force a stack rebuild instead of reusing a warm one
|
|
84
|
+
* @param {boolean} [options.recordBaseline] - Measure and write `expectedCount` back into
|
|
85
|
+
* `project.tests[]` — only from a run whose every test node is green (D8)
|
|
86
|
+
* @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
|
|
87
|
+
* of the tree (see linux-runner.js for why a bind mount would prove nothing)
|
|
88
|
+
* @param {string} [options.linuxImage] - Image override for --linux
|
|
57
89
|
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
58
90
|
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
59
91
|
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
@@ -69,6 +101,12 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
69
101
|
skipBuild = false,
|
|
70
102
|
skipE2e = false,
|
|
71
103
|
freshE2e = false,
|
|
104
|
+
recordBaseline = false,
|
|
105
|
+
refreshCache = false,
|
|
106
|
+
linux = false,
|
|
107
|
+
linuxImage = null,
|
|
108
|
+
_runLinuxSuite = runLinuxSuite,
|
|
109
|
+
_exec = undefined,
|
|
72
110
|
_runBuildCheck = runBuildCheck,
|
|
73
111
|
_runTestSuite = runTestSuite,
|
|
74
112
|
_runValidation = runValidation,
|
|
@@ -79,6 +117,64 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
79
117
|
const stack = readStack(projectPath);
|
|
80
118
|
const paths = readProjectPaths(projectPath);
|
|
81
119
|
|
|
120
|
+
// The suites are resolved ONCE, before anything is executed, because
|
|
121
|
+
// `--filter` has to be judged before the first second of build time is
|
|
122
|
+
// spent: a filter that matches nothing is a typo, and paying for a build to
|
|
123
|
+
// discover a typo is the worst of both outcomes.
|
|
124
|
+
const testPlan = resolveTestNodes(projectPath);
|
|
125
|
+
const filter = parseTestFilter(options.filter);
|
|
126
|
+
if (filter && !testPlan.nodes.some((node) => nodeMatchesFilter(node, filter))) {
|
|
127
|
+
const declared = testPlan.nodes.map((node) => node.id).join(', ') || '(none)';
|
|
128
|
+
throw new VerifyUsageError(
|
|
129
|
+
`--filter "${filter.raw}" matched no test node — declared nodes: ${declared}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
134
|
+
// discovering that there is nowhere to write AFTER running the whole fleet
|
|
135
|
+
// would charge the user a full suite for a usage error. `config.json` is
|
|
136
|
+
// never created here — this repository deliberately has none, and inventing
|
|
137
|
+
// one would break `doctor --full`.
|
|
138
|
+
if (recordBaseline) {
|
|
139
|
+
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
140
|
+
if (problem) throw new VerifyUsageError(problem);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// `--refresh-cache` is judged in the same breath as `--filter` and
|
|
144
|
+
// `--record-baseline`, and for the same reason — but here the stake is higher
|
|
145
|
+
// than a wasted clock: a refusal AFTER the cache was touched would leave a
|
|
146
|
+
// half-rewritten tree behind. Same discipline as `finish --merge`, which
|
|
147
|
+
// validates the root BEFORE archiving.
|
|
148
|
+
if (refreshCache) {
|
|
149
|
+
const problem = refreshCacheProblem(projectPath, testPlan, filter, { skipTests });
|
|
150
|
+
if (problem) throw new VerifyUsageError(problem);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `--linux` fails FAST and LOUD when Docker is missing (exit 2). Never a
|
|
154
|
+
// skip: "we could not check on Linux" reading as green is F35 itself.
|
|
155
|
+
if (linux) {
|
|
156
|
+
const problem = checkLinuxPreconditions({
|
|
157
|
+
projectPath, skipTests, ...(_exec ? { exec: _exec } : {}),
|
|
158
|
+
});
|
|
159
|
+
if (problem) throw new VerifyUsageError(problem);
|
|
160
|
+
|
|
161
|
+
// The MORPH_EVAL_* channel carries ABSOLUTE host paths, and the container
|
|
162
|
+
// runs a COPY of the tree at /work — so those paths do not exist inside it.
|
|
163
|
+
// Refusing by name is the only honest answer: running the node anyway would
|
|
164
|
+
// fail with an unrelated message, and skipping it would be the green-over-
|
|
165
|
+
// nothing shape this feature exists to remove. The escape is named.
|
|
166
|
+
const evalNode = testPlan.nodes.find(
|
|
167
|
+
(node) => node.runner === EVAL_RUNNER && nodeMatchesFilter(node, filter)
|
|
168
|
+
);
|
|
169
|
+
if (evalNode) {
|
|
170
|
+
throw new VerifyUsageError(
|
|
171
|
+
`--linux não executa o nó de eval "${evalNode.id}": o canal MORPH_EVAL_* carrega caminhos absolutos do host, `
|
|
172
|
+
+ 'e o container roda uma CÓPIA da árvore em /work. Rode o eval fora do container, '
|
|
173
|
+
+ `ou exclua-o com --filter (nós declarados: ${testPlan.nodes.map((n) => n.id).join(', ')}).`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
82
178
|
const nodes = {};
|
|
83
179
|
const feedback = [];
|
|
84
180
|
|
|
@@ -94,10 +190,18 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
94
190
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
95
191
|
nodes.tests = await runTestsNode({
|
|
96
192
|
projectPath,
|
|
193
|
+
testPlan,
|
|
194
|
+
filter,
|
|
97
195
|
skipTests,
|
|
98
196
|
buildNode: nodes.build,
|
|
99
197
|
timeout: explicitTimeout,
|
|
198
|
+
taskId,
|
|
199
|
+
refreshCache,
|
|
200
|
+
linux,
|
|
201
|
+
linuxImage,
|
|
100
202
|
_runTestSuite,
|
|
203
|
+
_runLinuxSuite,
|
|
204
|
+
_exec,
|
|
101
205
|
});
|
|
102
206
|
appendTestsFeedback(feedback, nodes.tests);
|
|
103
207
|
|
|
@@ -132,6 +236,13 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
132
236
|
|
|
133
237
|
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
134
238
|
|
|
239
|
+
// Recording happens LAST and only from what actually ran — the number is a
|
|
240
|
+
// measurement of this invocation, never a value carried in from anywhere.
|
|
241
|
+
const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
|
|
242
|
+
if (baseline && !baseline.recorded) {
|
|
243
|
+
feedback.push(`[baseline] não gravada: ${baseline.reason}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
135
246
|
return {
|
|
136
247
|
version: 1,
|
|
137
248
|
feature: featureName,
|
|
@@ -143,6 +254,7 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
143
254
|
nodes,
|
|
144
255
|
feedback,
|
|
145
256
|
traceability,
|
|
257
|
+
...(baseline ? { baseline } : {}),
|
|
146
258
|
};
|
|
147
259
|
}
|
|
148
260
|
|
|
@@ -186,35 +298,276 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
186
298
|
// Tests node
|
|
187
299
|
// ============================================================================
|
|
188
300
|
|
|
189
|
-
|
|
190
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Run EVERY test suite the project declares, and roll the results up.
|
|
303
|
+
*
|
|
304
|
+
* Why a rollup instead of one node: defect F30. The resolver answered
|
|
305
|
+
* "dotnet test", the 3.082 vitest tests of the same repository never ran, and
|
|
306
|
+
* the gate reported green — a suite nobody could name is a suite that cannot
|
|
307
|
+
* fail. `resolveTestNodes()` now returns the LIST, each entry runs with its own
|
|
308
|
+
* command, cwd and clock, and every one of them appears in `runs[]`.
|
|
309
|
+
*
|
|
310
|
+
* `nodes.tests` deliberately stays an OBJECT with `status` (D6): verify.js and
|
|
311
|
+
* the persisted stamp read `nodes.tests.status`, and turning a verification
|
|
312
|
+
* defect into a shape defect is not progress. The plural detail lives in
|
|
313
|
+
* `runs[]` beside it.
|
|
314
|
+
*
|
|
315
|
+
* @returns {Object} The rollup: `status`, joined `command`, summed `durationMs`,
|
|
316
|
+
* `source`, `environment` (null outside a container) and `runs[]`.
|
|
317
|
+
*/
|
|
318
|
+
async function runTestsNode({
|
|
319
|
+
projectPath, testPlan, filter, skipTests, buildNode, timeout, taskId, refreshCache = false,
|
|
320
|
+
linux = false, linuxImage = null, _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
321
|
+
}) {
|
|
322
|
+
const { nodes, source } = testPlan;
|
|
323
|
+
// Kept readable for whoever only prints the string — the same join the
|
|
324
|
+
// fallback scan already used for a chain of test projects.
|
|
325
|
+
const command = nodes.map((n) => n.command).filter(Boolean).join(' && ') || null;
|
|
326
|
+
// A fresh `runs: []` per return — two rollups must never alias one array.
|
|
327
|
+
const nothingRan = (reason) => ({
|
|
328
|
+
status: 'skip', command, source, environment: null, runs: [], warnings: [],
|
|
329
|
+
durationMs: 0, reason, output: '',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (skipTests) return nothingRan('requested');
|
|
191
333
|
|
|
192
|
-
if (
|
|
193
|
-
return
|
|
334
|
+
if (buildNode.status === 'fail') {
|
|
335
|
+
return nothingRan('build failed — test run would only add noise');
|
|
194
336
|
}
|
|
195
337
|
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
338
|
+
if (linux) {
|
|
339
|
+
const { runs, environment } = await runNodesInLinux({
|
|
340
|
+
projectPath, nodes, filter, taskId, timeout, linuxImage, _runLinuxSuite, _exec,
|
|
341
|
+
});
|
|
342
|
+
return rollUpTestRuns(runs, { command, source, environment });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const runs = [];
|
|
346
|
+
for (const node of nodes) {
|
|
347
|
+
runs.push(await runOneTestNode({
|
|
348
|
+
projectPath, node, filter, taskId, timeout, refreshCache, _runTestSuite,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return rollUpTestRuns(runs, { command, source });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Run the executable nodes inside ONE Linux container and judge them with the
|
|
357
|
+
* SAME rules the local path uses.
|
|
358
|
+
*
|
|
359
|
+
* The pre-decisions (invalid config, `--filter`, an e2e node at task scope)
|
|
360
|
+
* happen HERE, before the container exists: a node that must not run must not
|
|
361
|
+
* be written into the container script either, or the flag would quietly run
|
|
362
|
+
* what the user filtered out. What the container returns — an exit code and an
|
|
363
|
+
* output — is then handed to `judgeRun()`, the same function the host path
|
|
364
|
+
* calls, so a count drop means a count drop in both environments.
|
|
365
|
+
*
|
|
366
|
+
* The tree the container executes is a COPY on its own filesystem, never the
|
|
367
|
+
* bind mount: see linux-runner.js. That is the whole reason this path exists.
|
|
368
|
+
*/
|
|
369
|
+
async function runNodesInLinux({
|
|
370
|
+
projectPath, nodes, filter, taskId, timeout, linuxImage, _runLinuxSuite, _exec,
|
|
371
|
+
}) {
|
|
372
|
+
const decided = new Map();
|
|
373
|
+
const toRun = [];
|
|
374
|
+
for (const node of nodes) {
|
|
375
|
+
const pre = preDecideRun({ node, filter, taskId, projectPath });
|
|
376
|
+
if (pre) decided.set(node.id, pre);
|
|
377
|
+
else toRun.push(node);
|
|
204
378
|
}
|
|
205
379
|
|
|
380
|
+
// One container, one clock: the ceiling is the sum of what each node would
|
|
381
|
+
// have been given locally, since they share the run.
|
|
382
|
+
const containerTimeout = toRun.reduce(
|
|
383
|
+
(sum, node) => sum + resolveTestTimeout(projectPath, node, timeout), 0
|
|
384
|
+
) || undefined;
|
|
385
|
+
|
|
386
|
+
const suite = await _runLinuxSuite({
|
|
387
|
+
projectPath,
|
|
388
|
+
nodes: toRun,
|
|
389
|
+
image: linuxImage || undefined,
|
|
390
|
+
timeout: containerTimeout,
|
|
391
|
+
...(_exec ? { exec: _exec } : {}),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const byId = new Map((suite.results || []).map((r) => [r.id, r]));
|
|
395
|
+
const runs = nodes.map((node) => {
|
|
396
|
+
if (decided.has(node.id)) return decided.get(node.id);
|
|
397
|
+
const raw = byId.get(node.id);
|
|
398
|
+
if (!raw) {
|
|
399
|
+
// The container answered about a fleet that does not include this node.
|
|
400
|
+
// Silence here would be a suite vanishing from the report.
|
|
401
|
+
return { ...emptyRunEntry(node), status: 'fail', reason: 'o container não reportou este nó' };
|
|
402
|
+
}
|
|
403
|
+
if (raw.skipped) {
|
|
404
|
+
return { ...emptyRunEntry(node), reason: raw.reason };
|
|
405
|
+
}
|
|
406
|
+
const result = raw.exitCode === null
|
|
407
|
+
// No exit code is NOT a pass: the container died before this node
|
|
408
|
+
// reported, and a green verdict over an unobserved suite is F30's shape.
|
|
409
|
+
? { passed: false, output: raw.output || '', reason: raw.reason, exitCode: null }
|
|
410
|
+
: { passed: raw.exitCode === 0, output: raw.output || '', exitCode: raw.exitCode };
|
|
411
|
+
return judgeRun({ node, result, durationMs: raw.durationMs ?? 0 });
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return { runs, environment: suite.environment };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Execute (or decline to execute) one node, always producing a `runs[]` entry.
|
|
419
|
+
*
|
|
420
|
+
* Nothing is ever dropped: a node that must not run says so with a reason, in
|
|
421
|
+
* the place the reader is already looking. A suite that silently disappears
|
|
422
|
+
* from the report is the defect this whole feature exists to close.
|
|
423
|
+
*/
|
|
424
|
+
async function runOneTestNode({
|
|
425
|
+
projectPath, node, filter, taskId, timeout, refreshCache = false, _runTestSuite,
|
|
426
|
+
}) {
|
|
427
|
+
const decided = preDecideRun({ node, filter, taskId, projectPath, refreshCache });
|
|
428
|
+
if (decided) return decided;
|
|
429
|
+
|
|
430
|
+
// The eval channel: the CLI DICTATES where the cache, the dataset and the
|
|
431
|
+
// summary live, so the same xunit suite is runnable by `verify` without a
|
|
432
|
+
// second configuration path. `env` is undefined for every other runner, which
|
|
433
|
+
// keeps `runTestSuite` on exactly the path it had before.
|
|
434
|
+
const evalChannel = node.runner === EVAL_RUNNER
|
|
435
|
+
? evalNodeEnv(node, projectPath, { refresh: refreshCache })
|
|
436
|
+
: null;
|
|
437
|
+
|
|
206
438
|
const start = Date.now();
|
|
207
|
-
const result = await _runTestSuite(projectPath, {
|
|
208
|
-
|
|
439
|
+
const result = await _runTestSuite(projectPath, {
|
|
440
|
+
timeout, node, ...(evalChannel ? { env: evalChannel.env } : {}),
|
|
441
|
+
});
|
|
442
|
+
const entry = judgeRun({ node, result, durationMs: Date.now() - start });
|
|
443
|
+
|
|
444
|
+
return evalChannel ? judgeEvalRun(entry, node, evalChannel.summaryPath) : entry;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Second half of an eval verdict: what the SUMMARY says, on top of what the exit
|
|
449
|
+
* code said.
|
|
450
|
+
*
|
|
451
|
+
* Two things the exit code cannot see:
|
|
452
|
+
* - the scenario COUNT, which is what `expectedCount` is compared against.
|
|
453
|
+
* The summary wins when readable; otherwise the `dotnet` extractor's number
|
|
454
|
+
* stands, because an eval IS `dotnet test`. **null stays UNKNOWN, never 0** —
|
|
455
|
+
* an unreadable summary must not manufacture a count drop;
|
|
456
|
+
* - a suite that evaluated ZERO scenarios and exited 0. Nothing failed, because
|
|
457
|
+
* nothing ran: the guardrail-inert shape this repository has already paid to
|
|
458
|
+
* learn to detect.
|
|
459
|
+
*/
|
|
460
|
+
function judgeEvalRun(entry, node, summaryPath) {
|
|
461
|
+
const summary = readEvalSummary(summaryPath);
|
|
462
|
+
const warnings = [...(entry.warnings || [])];
|
|
463
|
+
|
|
464
|
+
const cold = evalColdCacheWarning(summary);
|
|
465
|
+
if (cold) warnings.push(cold);
|
|
466
|
+
|
|
467
|
+
const fromSummary = evalScenarioCount(summary);
|
|
468
|
+
const count = fromSummary ?? entry.count;
|
|
469
|
+
const verdict = compareTestCount(count, node.expectedCount);
|
|
470
|
+
if (verdict.warning) warnings.push(verdict.warning);
|
|
471
|
+
|
|
472
|
+
// As falhas NOMEADAS entram no buffer de saida do no, e entram na FRENTE.
|
|
473
|
+
// `appendTestsFeedback` prefere `output` a `reason`, entao um nome so no
|
|
474
|
+
// `reason` nunca chegaria ao `feedback[]`; e a cauda do `dotnet` e ruido
|
|
475
|
+
// truncado exatamente onde o sumario ja tem a frase certa. Prepend, e nao
|
|
476
|
+
// substituicao, porque um crash do runner tambem vive nessa cauda.
|
|
477
|
+
const named = evalFailureLines(summary);
|
|
478
|
+
const withNames = named.length > 0
|
|
479
|
+
? { ...entry, output: [...named, ...(entry.output ? [entry.output] : [])].join('\n') }
|
|
480
|
+
: entry;
|
|
481
|
+
|
|
482
|
+
const empty = evalEmptyRunProblem(summary);
|
|
483
|
+
if (empty) {
|
|
484
|
+
return { ...withNames, status: 'fail', count, warnings, reason: empty };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (entry.status === 'pass' && verdict.verdict === 'drop') {
|
|
488
|
+
return { ...withNames, status: 'fail', count, warnings, reason: verdict.reason };
|
|
489
|
+
}
|
|
490
|
+
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
491
|
+
|
|
492
|
+
return { ...withNames, count, warnings };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The `runs[]` entry every path starts from — every field present, nothing implied. */
|
|
496
|
+
function emptyRunEntry(node) {
|
|
497
|
+
return {
|
|
498
|
+
id: node.id,
|
|
499
|
+
command: node.command,
|
|
500
|
+
cwd: node.cwd,
|
|
501
|
+
runner: node.runner,
|
|
502
|
+
expectedCount: node.expectedCount,
|
|
503
|
+
// Always present, so no reader has to tell "absent" from "unknown" by
|
|
504
|
+
// optional chaining. **null is UNKNOWN, never 0** (D7).
|
|
505
|
+
count: null,
|
|
506
|
+
status: 'skip',
|
|
507
|
+
durationMs: 0,
|
|
508
|
+
reason: null,
|
|
509
|
+
warnings: [],
|
|
510
|
+
output: '',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The verdicts that exist BEFORE anything is executed, or null when the node
|
|
516
|
+
* has to run to be judged.
|
|
517
|
+
*
|
|
518
|
+
* Shared by the local loop and by `--linux`, so a node that must not run is
|
|
519
|
+
* refused for the same reason and with the same words in both environments —
|
|
520
|
+
* two copies of this ordering is how a filtered node ends up invisible in one
|
|
521
|
+
* of them.
|
|
522
|
+
*/
|
|
523
|
+
function preDecideRun({ node, filter, taskId, projectPath = '.', refreshCache = false }) {
|
|
524
|
+
const entry = emptyRunEntry(node);
|
|
525
|
+
|
|
526
|
+
// The resolver already decided: invalid config (fail-closed, D4) or an
|
|
527
|
+
// explicit "no suites declared". Executing it would be asking a question that
|
|
528
|
+
// has been answered — and, for an invalid node, answering it wrongly.
|
|
529
|
+
if (node.status) {
|
|
530
|
+
return { ...entry, status: node.status, reason: node.reason };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Deliberately AFTER the pre-decided verdict above: `--filter` says "do not
|
|
534
|
+
// spend time on this suite", not "do not tell me my config is broken". An
|
|
535
|
+
// invalid node costs no clock and still reddens the gate (D4).
|
|
536
|
+
if (!nodeMatchesFilter(node, filter)) {
|
|
537
|
+
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (node.e2e && taskId) {
|
|
541
|
+
return { ...entry, reason: 'e2e node runs at feature scope only' };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// The eval pre-flight lives HERE, beside the other pre-decisions, so the host
|
|
545
|
+
// loop and the `--linux` path refuse for the same reason with the same words.
|
|
546
|
+
// Its three cache verdicts are FAILURES, never skips: a skip over an unusable
|
|
547
|
+
// cache is a green gate above a verification that never happened. The only
|
|
548
|
+
// skip it produces is scope — an eval suite does not pay for itself inside the
|
|
549
|
+
// per-task loop, and the result Gate 3 reads is the feature-scoped one.
|
|
550
|
+
if (node.runner === EVAL_RUNNER) {
|
|
551
|
+
const pre = evalPreflight(projectPath, node, { taskId, refresh: refreshCache });
|
|
552
|
+
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Turn ONE executor result into a `runs[]` entry — the count rule included.
|
|
560
|
+
*
|
|
561
|
+
* Called by both the local loop and the `--linux` container path on purpose:
|
|
562
|
+
* a drop, a rise and an unknown count must mean the same thing whether the
|
|
563
|
+
* suite ran on this host or inside a container. A second copy of this judgement
|
|
564
|
+
* is how two environments start disagreeing about the same suite.
|
|
565
|
+
*/
|
|
566
|
+
function judgeRun({ node, result, durationMs }) {
|
|
567
|
+
const entry = emptyRunEntry(node);
|
|
209
568
|
|
|
210
569
|
if (result.skipped) {
|
|
211
|
-
return {
|
|
212
|
-
status: 'skip',
|
|
213
|
-
command,
|
|
214
|
-
durationMs,
|
|
215
|
-
reason: result.reason || 'no test command detected',
|
|
216
|
-
output: '',
|
|
217
|
-
};
|
|
570
|
+
return { ...entry, durationMs, reason: result.reason || 'no test command detected' };
|
|
218
571
|
}
|
|
219
572
|
|
|
220
573
|
// A timeout has no verdict to report: the tail of a truncated log reads like
|
|
@@ -222,8 +575,8 @@ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTe
|
|
|
222
575
|
if (result.timedOut) {
|
|
223
576
|
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
224
577
|
return {
|
|
578
|
+
...entry,
|
|
225
579
|
status: 'fail',
|
|
226
|
-
command,
|
|
227
580
|
durationMs,
|
|
228
581
|
timedOut: true,
|
|
229
582
|
reason: result.message,
|
|
@@ -231,20 +584,222 @@ async function runTestsNode({ projectPath, skipTests, buildNode, timeout, _runTe
|
|
|
231
584
|
};
|
|
232
585
|
}
|
|
233
586
|
|
|
587
|
+
// How many tests actually ran, and how that compares to the recorded
|
|
588
|
+
// baseline. Read from the FULL output, before it is truncated to a tail —
|
|
589
|
+
// the summary line is the last thing a runner prints, but a red run's tail
|
|
590
|
+
// is chosen for the failures, not for the summary.
|
|
591
|
+
const parsed = parseTestCount(result.output, node.runner);
|
|
592
|
+
const count = parsed ? parsed.total : null;
|
|
593
|
+
const verdict = compareTestCount(count, node.expectedCount);
|
|
594
|
+
const warnings = verdict.warning ? [verdict.warning] : [];
|
|
595
|
+
|
|
596
|
+
// A drop is the one count verdict that fails a node by itself: tests that
|
|
597
|
+
// used to run no longer do. A RISE never fails (D7) — the suite grows with
|
|
598
|
+
// every task, and failing on growth teaches deleting tests.
|
|
599
|
+
if (verdict.verdict === 'drop' && result.passed) {
|
|
600
|
+
return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '' };
|
|
601
|
+
}
|
|
602
|
+
// Already red: the failing test is the better lead, so the drop travels
|
|
603
|
+
// beside it instead of overwriting the reason.
|
|
604
|
+
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
605
|
+
|
|
234
606
|
return {
|
|
607
|
+
...entry,
|
|
235
608
|
status: result.passed ? 'pass' : 'fail',
|
|
236
|
-
command,
|
|
237
609
|
durationMs,
|
|
238
|
-
|
|
610
|
+
count,
|
|
611
|
+
warnings,
|
|
612
|
+
// `test-build-failed` / `stale-binary`: the executor refused BEFORE any
|
|
613
|
+
// test ran, and the reason is the only thing worth reading. Without this
|
|
614
|
+
// the node would go red with a null reason and the reader would go hunting
|
|
615
|
+
// through a compiler log for a suite that never started.
|
|
616
|
+
reason: result.reason ?? null,
|
|
239
617
|
output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
|
|
240
618
|
};
|
|
241
619
|
}
|
|
242
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Aggregate the per-node runs into the single node `verify` publishes.
|
|
623
|
+
*
|
|
624
|
+
* The rollup is pessimistic on purpose: ANY red run reddens it. A green
|
|
625
|
+
* aggregate covering a red suite is F30 restated, so `some(fail)` is checked
|
|
626
|
+
* before anything else. `skip` only survives when there is nothing else to
|
|
627
|
+
* report — one green run among skips is a pass.
|
|
628
|
+
*/
|
|
629
|
+
function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
630
|
+
const failed = runs.filter((r) => r.status === 'fail');
|
|
631
|
+
const status = failed.length > 0
|
|
632
|
+
? 'fail'
|
|
633
|
+
: runs.some((r) => r.status === 'pass') ? 'pass' : 'skip';
|
|
634
|
+
|
|
635
|
+
const node = {
|
|
636
|
+
status,
|
|
637
|
+
command,
|
|
638
|
+
durationMs: runs.reduce((sum, r) => sum + (r.durationMs || 0), 0),
|
|
639
|
+
reason: null,
|
|
640
|
+
output: failed.map((r) => r.output).filter(Boolean).join('\n'),
|
|
641
|
+
source,
|
|
642
|
+
// Where these suites ran. `null` is the local host; a container run says so
|
|
643
|
+
// by name and image, because a green from a container and a green from the
|
|
644
|
+
// host are otherwise indistinguishable in feature.json — and the next
|
|
645
|
+
// person would not know what was proven.
|
|
646
|
+
environment,
|
|
647
|
+
// Named by suite: a warning that does not say WHICH baseline went stale
|
|
648
|
+
// sends the reader to read every one of them.
|
|
649
|
+
warnings: runs.flatMap((r) => (r.warnings || []).map((w) => `${r.id}: ${w}`)),
|
|
650
|
+
runs,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
if (status === 'fail') node.reason = failed[0].reason ?? null;
|
|
654
|
+
else if (status === 'skip') {
|
|
655
|
+
node.reason = runs.length === 1 ? runs[0].reason : 'every test node was skipped';
|
|
656
|
+
}
|
|
657
|
+
if (runs.some((r) => r.timedOut)) node.timedOut = true;
|
|
658
|
+
|
|
659
|
+
return node;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* One feedback line per RED node, named by node id.
|
|
664
|
+
*
|
|
665
|
+
* The `[tests/{id}]` prefix replaces the old bare `[tests]`: with N suites, a
|
|
666
|
+
* line that does not say which one failed sends the reader to the wrong repo
|
|
667
|
+
* half.
|
|
668
|
+
*/
|
|
243
669
|
function appendTestsFeedback(feedback, testsNode) {
|
|
670
|
+
// Warnings are emitted whatever the verdict — a stale baseline lives on a
|
|
671
|
+
// GREEN node by construction (the count rose), so a warning that only
|
|
672
|
+
// printed on failure would never print at all.
|
|
673
|
+
for (const run of testsNode.runs || []) {
|
|
674
|
+
for (const warning of run.warnings || []) {
|
|
675
|
+
feedback.push(`[tests/${run.id}] warning: ${warning}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
244
678
|
if (testsNode.status !== 'fail') return;
|
|
245
|
-
for (const
|
|
246
|
-
|
|
679
|
+
for (const run of testsNode.runs || []) {
|
|
680
|
+
if (run.status !== 'fail') continue;
|
|
681
|
+
for (const line of linesOf(run.output || run.reason || '')) {
|
|
682
|
+
feedback.push(`[tests/${run.id}] ${line}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Why `--refresh-cache` cannot mean what it says, or null when it can.
|
|
689
|
+
*
|
|
690
|
+
* Judged BEFORE the build node, and the ordering is the whole point: a refusal
|
|
691
|
+
* that arrived after the cache was touched would leave a half-rewritten tree
|
|
692
|
+
* behind. Same discipline `finish --merge` learned — validate the precondition
|
|
693
|
+
* before the mutation, never after.
|
|
694
|
+
*
|
|
695
|
+
* Three refusals, all usage errors (exit 2), never verdicts:
|
|
696
|
+
* - no eval node declared: succeeding silently would teach the user to type a
|
|
697
|
+
* flag that does nothing;
|
|
698
|
+
* - `--skip-tests`: the two flags cancel each other, and the winner would be
|
|
699
|
+
* whichever the code happened to check first;
|
|
700
|
+
* - a filter that excludes every eval node: same shape as the first.
|
|
701
|
+
*
|
|
702
|
+
* The MISSING CREDENTIAL is deliberately NOT judged here: it is per node (each
|
|
703
|
+
* declares its own `envFile`), so `evalPreflight` answers it — still before the
|
|
704
|
+
* command runs, and still without touching the tree.
|
|
705
|
+
*
|
|
706
|
+
* @param {string} projectPath
|
|
707
|
+
* @param {{nodes: Array}} testPlan
|
|
708
|
+
* @param {{tokens: string[], raw: string}|null} filter
|
|
709
|
+
* @param {{skipTests: boolean}} opts
|
|
710
|
+
* @returns {string|null}
|
|
711
|
+
*/
|
|
712
|
+
function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
713
|
+
if (skipTests) {
|
|
714
|
+
return '--refresh-cache com --skip-tests: as duas flags se anulam — a regravação da cache É a suíte de eval';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const evalNodes = testPlan.nodes.filter((node) => node.runner === EVAL_RUNNER);
|
|
718
|
+
if (evalNodes.length === 0) {
|
|
719
|
+
const declared = testPlan.nodes.map((node) => node.id).join(', ') || '(nenhum)';
|
|
720
|
+
return `--refresh-cache sem nenhum nó \`runner: "evals"\` declarado em project.tests[] — nós declarados: ${declared}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!evalNodes.some((node) => nodeMatchesFilter(node, filter))) {
|
|
724
|
+
return `--refresh-cache com --filter "${filter.raw}", que não casa nenhum nó de eval`
|
|
725
|
+
+ ` (nós de eval: ${evalNodes.map((node) => node.id).join(', ')})`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ============================================================================
|
|
732
|
+
// Baseline recording (--record-baseline)
|
|
733
|
+
// ============================================================================
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Why `--record-baseline` cannot run here, or null when it can.
|
|
737
|
+
*
|
|
738
|
+
* Judged BEFORE the build node, beside the `--filter` check, because both are
|
|
739
|
+
* statements about a command that cannot mean what it says. Paying for a full
|
|
740
|
+
* suite to be told there was nowhere to write the result is the worst possible
|
|
741
|
+
* ordering.
|
|
742
|
+
*
|
|
743
|
+
* The refusal for a project with no `project.tests[]` is deliberate rather
|
|
744
|
+
* than helpful: the nodes the fallback synthesizes are not declared anywhere,
|
|
745
|
+
* so there is no entry to update, and writing one would mean this command
|
|
746
|
+
* inventing the declaration it came to maintain. `config.json` is likewise
|
|
747
|
+
* never created — the framework repository has none on purpose (creating it
|
|
748
|
+
* breaks `doctor --full`), so the honest answer is a named usage error.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} projectPath
|
|
751
|
+
* @param {{ nodes: Array, source: string }} testPlan
|
|
752
|
+
* @param {{ skipTests: boolean }} opts
|
|
753
|
+
* @returns {string|null}
|
|
754
|
+
*/
|
|
755
|
+
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
756
|
+
if (skipTests) {
|
|
757
|
+
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
247
758
|
}
|
|
759
|
+
if (!existsSync(morphConfigPath(projectPath))) {
|
|
760
|
+
return `--record-baseline não tem onde gravar: ${morphConfigPath(projectPath)} não existe `
|
|
761
|
+
+ '(e não será criado — declare project.tests[] antes)';
|
|
762
|
+
}
|
|
763
|
+
if (testPlan.source !== 'config-tests') {
|
|
764
|
+
return '--record-baseline exige project.tests[] declarado em .morph/config/config.json — '
|
|
765
|
+
+ `os nós de "${testPlan.source}" são sintetizados pelo fallback e não têm entrada onde gravar`;
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Turn a finished tests rollup into a baseline write, or into a refusal.
|
|
772
|
+
*
|
|
773
|
+
* D8 lives here: the number is only ever what a REAL run measured, and only
|
|
774
|
+
* when every node of that run is green. A red run records nothing — writing a
|
|
775
|
+
* lower count over a regression is how the regression becomes the new normal.
|
|
776
|
+
* A PARTIAL run records nothing either: `--filter` and skips leave nodes
|
|
777
|
+
* unmeasured, and a baseline covering half the fleet is a claim about the
|
|
778
|
+
* other half that nobody made.
|
|
779
|
+
*
|
|
780
|
+
* @param {string} projectPath
|
|
781
|
+
* @param {Object} testsNode - The rollup produced by runTestsNode()
|
|
782
|
+
* @returns {{ recorded: boolean, reason: string|null, updates: Array, path?: string }}
|
|
783
|
+
*/
|
|
784
|
+
function recordBaselineFrom(projectPath, testsNode) {
|
|
785
|
+
const runs = testsNode.runs || [];
|
|
786
|
+
const refuse = (reason) => ({ recorded: false, reason, updates: [] });
|
|
787
|
+
|
|
788
|
+
if (runs.length === 0) return refuse('nenhum nó de teste executou');
|
|
789
|
+
|
|
790
|
+
const notGreen = runs.filter((r) => r.status !== 'pass');
|
|
791
|
+
if (notGreen.length > 0) {
|
|
792
|
+
const named = notGreen.map((r) => `${r.id} (${r.status}${r.reason ? `: ${r.reason}` : ''})`).join('; ');
|
|
793
|
+
return refuse(
|
|
794
|
+
`execução não está inteiramente verde — ${named}. `
|
|
795
|
+
+ 'Gravar baseline de execução vermelha ou parcial congela a regressão'
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return recordTestBaselines(
|
|
800
|
+
projectPath,
|
|
801
|
+
runs.map((r) => ({ id: r.id, count: r.count }))
|
|
802
|
+
);
|
|
248
803
|
}
|
|
249
804
|
|
|
250
805
|
// ============================================================================
|
|
@@ -354,10 +909,17 @@ async function runE2eNode({ projectPath, featureName, taskId, skipE2e, freshE2e,
|
|
|
354
909
|
durationMs,
|
|
355
910
|
reason: result.reason ?? null,
|
|
356
911
|
output: result.output || '',
|
|
912
|
+
// Non-fatal findings (e.g. a compose with fixed ports at the ROOT, where
|
|
913
|
+
// nothing collides yet). Surfaced so the same compose does not fail
|
|
914
|
+
// unannounced the day the feature moves into a worktree.
|
|
915
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
357
916
|
};
|
|
358
917
|
}
|
|
359
918
|
|
|
360
919
|
function appendE2eFeedback(feedback, e2eNode) {
|
|
920
|
+
for (const warning of e2eNode.warnings || []) {
|
|
921
|
+
feedback.push(`[e2e] warning: ${warning}`);
|
|
922
|
+
}
|
|
361
923
|
if (e2eNode.status !== 'fail') return;
|
|
362
924
|
if (e2eNode.reason) feedback.push(`[e2e] ${e2eNode.reason}`);
|
|
363
925
|
for (const line of linesOf(e2eNode.output)) {
|