@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,41 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verify Runner — deterministic build+tests+validators+e2e cadence.
|
|
3
|
-
*
|
|
4
|
-
* v8 adds a DETERMINISTIC verification node that runs BEFORE the LLM judge
|
|
5
|
-
* in the per-task loop: build + tests + validators (code, free) first; the
|
|
6
|
-
* LLM judge is only invoked once verify passes. This is a BINARY gate
|
|
7
|
-
* (pass/fail per node).
|
|
8
|
-
*
|
|
9
|
-
* The `e2e` node closes the last gap in that chain: build proves the code
|
|
10
|
-
* COMPILES, tests prove units behave, validators read structure — none of
|
|
11
|
-
* them prove the application RUNS. See e2e-check.js for why that distinction
|
|
12
|
-
* is load-bearing rather than academic.
|
|
13
|
-
*
|
|
14
|
-
* Pure orchestration — reuses (never reimplements):
|
|
15
|
-
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
16
|
-
* - runTestSuite() from src/lib/tasks/test-runner.js (one node)
|
|
17
|
-
* - resolveTestNodes() from src/lib/tasks/test-nodes.js (the LIST of suites)
|
|
18
|
-
* - applyTaskScope() from src/lib/tasks/test-scope.js (the task-scoped cut, #87)
|
|
19
|
-
* - runValidation() from ./validation-runner.js
|
|
20
|
-
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
21
|
-
*
|
|
22
|
-
* @module verify-runner
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
1
|
import { execFileSync } from 'child_process';
|
|
26
2
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
27
3
|
import { join, resolve } from 'path';
|
|
28
4
|
|
|
29
|
-
import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
30
|
-
import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
5
|
+
import { runBuildCheck, getBuildSteps, isDotnetStack } from '../../../framework/hooks/shared/build-check.js';
|
|
6
|
+
import { composeProjectName, runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
31
7
|
import { buildComposeEnv } from '../e2e/compose-env.js';
|
|
32
8
|
import {
|
|
33
|
-
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout,
|
|
9
|
+
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, discoverDotnetSolution,
|
|
34
10
|
} from '../tasks/test-runner.js';
|
|
35
11
|
import {
|
|
36
12
|
resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
|
|
37
13
|
} from '../tasks/test-nodes.js';
|
|
38
|
-
import { applyTaskScope } from '../tasks/test-scope.js';
|
|
39
14
|
import { runLinuxSuite, checkLinuxPreconditions } from '../tasks/linux-runner.js';
|
|
40
15
|
import {
|
|
41
16
|
EVAL_RUNNER, evalPreflight, evalNodeEnv, readEvalSummary, evalScenarioCount,
|
|
@@ -44,35 +19,16 @@ import {
|
|
|
44
19
|
} from '../tasks/eval-node.js';
|
|
45
20
|
import { runValidation } from './validation-runner.js';
|
|
46
21
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
47
|
-
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
48
22
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
49
23
|
|
|
50
|
-
|
|
24
|
+
// A dotnet cold build is slow; the Stop-hook default of 30s is too short.
|
|
51
25
|
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
52
26
|
|
|
53
|
-
// Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
|
|
54
27
|
const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
55
28
|
|
|
56
|
-
/** Number of trailing output lines kept for feedback/reporting. */
|
|
57
29
|
const OUTPUT_TAIL_LINES = 20;
|
|
58
30
|
|
|
59
|
-
/** A
|
|
60
|
-
const NARROWED_ZERO_TESTS = 'narrowed-zero-tests: o recorte executou 0 testes';
|
|
61
|
-
const NARROWED_ZERO_TESTS_LINK = 'narrowed-zero-tests: um link do recorte executou 0 testes (bloco MTP com total: 0)';
|
|
62
|
-
|
|
63
|
-
/** A narrowed run that exited green with no count anyone could read (#87): null is UNKNOWN, and unknown is never green. */
|
|
64
|
-
const NARROWED_COUNT_UNKNOWN = 'narrowed-count-unknown: o recorte saiu verde sem contagem legível — '
|
|
65
|
-
+ 'nada prova que algum teste rodou (desconhecido nunca vira verde)';
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The caller asked for something impossible — not a red verification, a wrong
|
|
69
|
-
* invocation. The distinction is the exit code: `verify` answers 1 for "the
|
|
70
|
-
* code is not ready" and 2 for "this command cannot mean what you typed".
|
|
71
|
-
*
|
|
72
|
-
* It exists because `--filter` can select nothing. Running every suite (or,
|
|
73
|
-
* far worse, none of them) and reporting green would be F30 restated as a
|
|
74
|
-
* one-word typo — so the runner refuses instead of guessing.
|
|
75
|
-
*/
|
|
31
|
+
/** A wrong invocation, not a red verification: `verify` exits 2 for it, 1 for "code not ready". */
|
|
76
32
|
export class VerifyUsageError extends Error {
|
|
77
33
|
constructor(message) {
|
|
78
34
|
super(message);
|
|
@@ -82,44 +38,19 @@ export class VerifyUsageError extends Error {
|
|
|
82
38
|
}
|
|
83
39
|
|
|
84
40
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* @param {string} projectPath - Project root directory
|
|
88
|
-
* @param {string} featureName - Feature name
|
|
89
|
-
* @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
|
|
90
|
-
* @param {Object} [options]
|
|
91
|
-
* @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
|
|
92
|
-
* (default: 120000 for build; for tests, see resolveTestTimeout())
|
|
93
|
-
* @param {boolean} [options.skipTests] - Skip the tests node
|
|
94
|
-
* @param {boolean} [options.skipBuild] - Skip the build node
|
|
95
|
-
* @param {boolean} [options.skipE2e] - Skip the e2e node
|
|
96
|
-
* @param {boolean} [options.freshE2e] - Force a stack rebuild instead of reusing a warm one
|
|
97
|
-
* @param {boolean} [options.recordBaseline] - Measure and write `expectedCount` back into
|
|
98
|
-
* `project.tests[]` — only from a run whose every test node is green (D8)
|
|
99
|
-
* @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
|
|
100
|
-
* of the tree (see linux-runner.js for why a bind mount would prove nothing)
|
|
101
|
-
* @param {string} [options.linuxImage] - Image override for --linux
|
|
102
|
-
* @param {boolean} [options.full] - Task scope only: switch the recorte off and run every
|
|
103
|
-
* node whole (#87). A documented no-op at feature scope, which never narrows anything.
|
|
104
|
-
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
105
|
-
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
106
|
-
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
107
|
-
* @param {Function} [options._runE2eCheck] - Injectable override for runE2eCheck (testing)
|
|
108
|
-
* @param {Function} [options._checkOutputsTraceability] - Injectable override for checkOutputsTraceability (testing)
|
|
109
|
-
* @param {Object} [options._heavyGate] - Injectable heavy-slot gate (testing); default is one per run
|
|
110
|
-
* @returns {Promise<Object>} Aggregated verify result — see module docs for shape
|
|
41
|
+
* @throws {VerifyUsageError} on an invocation that cannot mean what was typed (exit 2)
|
|
111
42
|
* @throws {HeavyBusyError} when no heavy slot (or RAM) freed within `heavy.waitTimeoutSec`
|
|
112
43
|
*/
|
|
113
|
-
export async function runVerify(projectPath, featureName,
|
|
44
|
+
export async function runVerify(projectPath, featureName, options = {}) {
|
|
114
45
|
const gate = options._heavyGate ?? createHeavyGate({ projectPath, feature: featureName });
|
|
115
46
|
try {
|
|
116
|
-
return await runVerifyGated(projectPath, featureName,
|
|
47
|
+
return await runVerifyGated(projectPath, featureName, options, gate);
|
|
117
48
|
} finally {
|
|
118
49
|
gate.release();
|
|
119
50
|
}
|
|
120
51
|
}
|
|
121
52
|
|
|
122
|
-
async function runVerifyGated(projectPath, featureName,
|
|
53
|
+
async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
123
54
|
const {
|
|
124
55
|
timeout = DEFAULT_BUILD_TIMEOUT,
|
|
125
56
|
skipTests = false,
|
|
@@ -130,7 +61,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
130
61
|
refreshCache = false,
|
|
131
62
|
linux = false,
|
|
132
63
|
linuxImage = null,
|
|
133
|
-
full = false,
|
|
134
64
|
_runLinuxSuite = runLinuxSuite,
|
|
135
65
|
_exec = undefined,
|
|
136
66
|
_runBuildCheck = runBuildCheck,
|
|
@@ -143,10 +73,8 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
143
73
|
const stack = readStack(projectPath);
|
|
144
74
|
const paths = readProjectPaths(projectPath);
|
|
145
75
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// spent: a filter that matches nothing is a typo, and paying for a build to
|
|
149
|
-
// discover a typo is the worst of both outcomes.
|
|
76
|
+
// Usage errors are judged before the build: paying for a build to discover a typo
|
|
77
|
+
// is the worst ordering, and a refusal after the cache was touched leaves a half-rewritten tree.
|
|
150
78
|
const testPlan = resolveTestNodes(projectPath);
|
|
151
79
|
const filter = parseTestFilter(options.filter);
|
|
152
80
|
if (filter && !testPlan.nodes.some((node) => nodeMatchesFilter(node, filter))) {
|
|
@@ -156,44 +84,23 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
156
84
|
);
|
|
157
85
|
}
|
|
158
86
|
|
|
159
|
-
// The task is read ONCE: its group picks the e2e applicability and, with its
|
|
160
|
-
// outputs, the test scope. One reader of tasks.json for verify, so the two
|
|
161
|
-
// can never disagree about the task.
|
|
162
|
-
const task = taskId ? readTask(projectPath, featureName, taskId) : null;
|
|
163
|
-
|
|
164
|
-
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
165
|
-
// discovering that there is nowhere to write AFTER running the whole fleet
|
|
166
|
-
// would charge the user a full suite for a usage error. `config.json` is
|
|
167
|
-
// never created here — this repository deliberately has none, and inventing
|
|
168
|
-
// one would break `doctor --full`.
|
|
169
87
|
if (recordBaseline) {
|
|
170
|
-
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests
|
|
88
|
+
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
171
89
|
if (problem) throw new VerifyUsageError(problem);
|
|
172
90
|
}
|
|
173
91
|
|
|
174
|
-
// `--refresh-cache` is judged in the same breath as `--filter` and
|
|
175
|
-
// `--record-baseline`, and for the same reason — but here the stake is higher
|
|
176
|
-
// than a wasted clock: a refusal AFTER the cache was touched would leave a
|
|
177
|
-
// half-rewritten tree behind. Same discipline as `finish --merge`, which
|
|
178
|
-
// validates the root BEFORE archiving.
|
|
179
92
|
if (refreshCache) {
|
|
180
93
|
const problem = refreshCacheProblem(projectPath, testPlan, filter, { skipTests });
|
|
181
94
|
if (problem) throw new VerifyUsageError(problem);
|
|
182
95
|
}
|
|
183
96
|
|
|
184
|
-
//
|
|
185
|
-
// skip: "we could not check on Linux" reading as green is F35 itself.
|
|
97
|
+
// Missing Docker is a usage error, never a skip: "could not check on Linux" must not read as green.
|
|
186
98
|
if (linux) {
|
|
187
99
|
const problem = checkLinuxPreconditions({
|
|
188
100
|
projectPath, skipTests, ...(_exec ? { exec: _exec } : {}),
|
|
189
101
|
});
|
|
190
102
|
if (problem) throw new VerifyUsageError(problem);
|
|
191
103
|
|
|
192
|
-
// The MORPH_EVAL_* channel carries ABSOLUTE host paths, and the container
|
|
193
|
-
// runs a COPY of the tree at /work — so those paths do not exist inside it.
|
|
194
|
-
// Refusing by name is the only honest answer: running the node anyway would
|
|
195
|
-
// fail with an unrelated message, and skipping it would be the green-over-
|
|
196
|
-
// nothing shape this feature exists to remove. The escape is named.
|
|
197
104
|
const evalNode = testPlan.nodes.find(
|
|
198
105
|
(node) => node.runner === EVAL_RUNNER && nodeMatchesFilter(node, filter)
|
|
199
106
|
);
|
|
@@ -206,85 +113,63 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
206
113
|
}
|
|
207
114
|
}
|
|
208
115
|
|
|
209
|
-
// #87: at task scope the task's group picks the nodes and its outputs narrow
|
|
210
|
-
// the command inside each; test-scope.js DECIDES, this runner executes. The
|
|
211
|
-
// scoping happens ONCE, here, before runTestsNode, so the host loop and the
|
|
212
|
-
// `--linux` container receive the SAME scoped nodes (the container script is
|
|
213
|
-
// written from `node.command`) and preDecideRun/judgeRun stay the single copy
|
|
214
|
-
// of each rule for both. `--filter` was judged above on the ORIGINAL ids: a
|
|
215
|
-
// typo is a typo whatever the task. Feature scope gets the nodes back
|
|
216
|
-
// unchanged, because Gate 3 always runs every suite, whole.
|
|
217
|
-
const scoped = applyTaskScope({ projectPath, nodes: testPlan.nodes, task, taskId, filter, full });
|
|
218
|
-
|
|
219
116
|
const nodes = {};
|
|
220
117
|
const feedback = [];
|
|
221
118
|
|
|
222
|
-
// 1. build
|
|
223
119
|
nodes.build = await runBuildNode({ projectPath, stack, paths, timeout, skipBuild, gate, _runBuildCheck });
|
|
224
120
|
appendBuildFeedback(feedback, nodes.build);
|
|
225
121
|
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// typed one: `timeout` above is defaulted for the build, and passing that
|
|
229
|
-
// default down would silently outrank config.project.testTimeoutMs and the
|
|
230
|
-
// per-project scaling (see resolveTestTimeout()).
|
|
122
|
+
// Only a TYPED --timeout reaches the tests: the build default would outrank
|
|
123
|
+
// config.project.testTimeoutMs and the per-project scaling.
|
|
231
124
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
125
|
+
const stackNodes = [];
|
|
232
126
|
nodes.tests = await runTestsNode({
|
|
233
127
|
projectPath,
|
|
234
|
-
testPlan
|
|
128
|
+
testPlan,
|
|
235
129
|
filter,
|
|
236
130
|
skipTests,
|
|
237
131
|
buildNode: nodes.build,
|
|
238
132
|
timeout: explicitTimeout,
|
|
239
|
-
taskId,
|
|
240
133
|
refreshCache,
|
|
241
134
|
linux,
|
|
242
135
|
linuxImage,
|
|
243
136
|
gate,
|
|
137
|
+
stackNodes,
|
|
244
138
|
_runTestSuite,
|
|
245
139
|
_runLinuxSuite,
|
|
246
140
|
_exec,
|
|
247
141
|
});
|
|
248
|
-
|
|
249
|
-
// and nowhere else: nodeStatuses() (verify.js) persists status strings only,
|
|
250
|
-
// so no part of the selection ever reaches feature.json.
|
|
251
|
-
nodes.tests.scope = scoped.summary;
|
|
252
|
-
appendTestsFeedback(feedback, nodes.tests);
|
|
142
|
+
if (stackNodes.length === 0) appendTestsFeedback(feedback, nodes.tests);
|
|
253
143
|
|
|
254
|
-
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
255
144
|
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
256
145
|
appendValidatorsFeedback(feedback, nodes.validators);
|
|
257
146
|
|
|
258
|
-
|
|
259
|
-
// invoked at all on a red build: a stack that cannot compile cannot come up,
|
|
260
|
-
// and the docker output would bury the actual compile error.
|
|
261
|
-
nodes.e2e = await runE2eNode({
|
|
147
|
+
const e2e = await runE2eNode({
|
|
262
148
|
projectPath,
|
|
263
149
|
featureName,
|
|
264
|
-
taskId,
|
|
265
|
-
task,
|
|
266
150
|
skipE2e,
|
|
267
151
|
freshE2e,
|
|
268
152
|
buildNode: nodes.build,
|
|
269
153
|
gate,
|
|
270
154
|
_runE2eCheck,
|
|
271
155
|
});
|
|
156
|
+
nodes.e2e = e2e.node;
|
|
272
157
|
appendE2eFeedback(feedback, nodes.e2e);
|
|
273
158
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
159
|
+
if (stackNodes.length > 0) {
|
|
160
|
+
nodes.tests = await runStackTestNodes({
|
|
161
|
+
projectPath, testsNode: nodes.tests, stackNodes, e2eNode: nodes.e2e, stackEnv: e2e.stackEnv,
|
|
162
|
+
timeout: explicitTimeout, gate, _runTestSuite,
|
|
163
|
+
});
|
|
164
|
+
appendTestsFeedback(feedback, nodes.tests);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Outside `nodes` on purpose: status comes only from the nodes, so a traceability
|
|
168
|
+
// warning never turns red.
|
|
169
|
+
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
283
170
|
|
|
284
171
|
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
285
172
|
|
|
286
|
-
// Recording happens LAST and only from what actually ran — the number is a
|
|
287
|
-
// measurement of this invocation, never a value carried in from anywhere.
|
|
288
173
|
const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
|
|
289
174
|
if (baseline && !baseline.recorded) {
|
|
290
175
|
feedback.push(`[baseline] não gravada: ${baseline.reason}`);
|
|
@@ -293,8 +178,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
293
178
|
return {
|
|
294
179
|
version: 1,
|
|
295
180
|
feature: featureName,
|
|
296
|
-
task: taskId || null,
|
|
297
|
-
scope: taskId ? 'task' : 'feature',
|
|
298
181
|
stack,
|
|
299
182
|
status,
|
|
300
183
|
nodes,
|
|
@@ -304,16 +187,29 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
304
187
|
};
|
|
305
188
|
}
|
|
306
189
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
190
|
+
/**
|
|
191
|
+
* A root `dotnet build` with no solution at the root fails MSB1003: build the
|
|
192
|
+
* repository's single solution instead, or say which candidates and which key decide.
|
|
193
|
+
*/
|
|
194
|
+
export function resolveDotnetBuild(projectPath, stack, paths = {}) {
|
|
195
|
+
if (!stack || !isDotnetStack(stack.toLowerCase()) || paths.backendPath) return { paths, hint: null };
|
|
196
|
+
const { target, candidates } = discoverDotnetSolution(projectPath);
|
|
197
|
+
if (target === '.') return { paths, hint: null };
|
|
198
|
+
if (target) return { paths: { ...paths, backendTarget: target }, hint: null };
|
|
199
|
+
const found = candidates.length ? `candidatos: ${candidates.join(', ')}` : 'nenhum encontrado abaixo dela';
|
|
200
|
+
return {
|
|
201
|
+
paths,
|
|
202
|
+
hint: `nenhum .sln/.slnx/.csproj na raiz (${found}): declare project.backendPath em .morph/config/config.json com a pasta da solução, relativa à raiz`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
310
205
|
|
|
311
|
-
async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild, gate, _runBuildCheck }) {
|
|
206
|
+
async function runBuildNode({ projectPath, stack, paths: declaredPaths = {}, timeout, skipBuild, gate, _runBuildCheck }) {
|
|
312
207
|
if (skipBuild) {
|
|
313
208
|
return { status: 'skip', command: null, durationMs: 0, reason: 'requested', output: '' };
|
|
314
209
|
}
|
|
315
210
|
|
|
316
|
-
const
|
|
211
|
+
const { paths, hint } = resolveDotnetBuild(projectPath, stack, declaredPaths);
|
|
212
|
+
const steps = getBuildSteps(stack, { cwd: projectPath, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
317
213
|
if (steps.length === 0) {
|
|
318
214
|
const reason = stack ? `no build command for stack "${stack}"` : 'no stack configured';
|
|
319
215
|
return { status: 'skip', command: null, durationMs: 0, reason, output: '' };
|
|
@@ -330,7 +226,7 @@ async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild
|
|
|
330
226
|
command,
|
|
331
227
|
durationMs,
|
|
332
228
|
reason: result.status === 'skip' ? 'no build command for stack' : null,
|
|
333
|
-
output: result.output
|
|
229
|
+
output: [result.output, result.status === 'fail' ? hint : null].filter(Boolean).join('\n'),
|
|
334
230
|
};
|
|
335
231
|
}
|
|
336
232
|
|
|
@@ -341,34 +237,13 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
341
237
|
}
|
|
342
238
|
}
|
|
343
239
|
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// ============================================================================
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Run EVERY test suite the project declares, and roll the results up.
|
|
350
|
-
*
|
|
351
|
-
* Why a rollup instead of one node: defect F30. The resolver answered
|
|
352
|
-
* "dotnet test", the 3.082 vitest tests of the same repository never ran, and
|
|
353
|
-
* the gate reported green — a suite nobody could name is a suite that cannot
|
|
354
|
-
* fail. `resolveTestNodes()` now returns the LIST, each entry runs with its own
|
|
355
|
-
* command, cwd and clock, and every one of them appears in `runs[]`.
|
|
356
|
-
*
|
|
357
|
-
* `nodes.tests` deliberately stays an OBJECT with `status` (D6): verify.js and
|
|
358
|
-
* the persisted stamp read `nodes.tests.status`, and turning a verification
|
|
359
|
-
* defect into a shape defect is not progress. The plural detail lives in
|
|
360
|
-
* `runs[]` beside it.
|
|
361
|
-
*
|
|
362
|
-
* @returns {Object} The rollup: `status`, joined `command`, summed `durationMs`,
|
|
363
|
-
* `source`, `environment` (null outside a container) and `runs[]`.
|
|
364
|
-
*/
|
|
240
|
+
// Every declared suite runs and appears in `runs[]`; the rollup stays an object
|
|
241
|
+
// with `status` because verify.js and the persisted stamp read `nodes.tests.status`.
|
|
365
242
|
async function runTestsNode({
|
|
366
|
-
projectPath, testPlan, filter, skipTests, buildNode, timeout,
|
|
367
|
-
linux = false, linuxImage = null, gate, _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
243
|
+
projectPath, testPlan, filter, skipTests, buildNode, timeout, refreshCache = false,
|
|
244
|
+
linux = false, linuxImage = null, gate, stackNodes = [], _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
368
245
|
}) {
|
|
369
246
|
const { nodes, source } = testPlan;
|
|
370
|
-
// Kept readable for whoever only prints the string — the same join the
|
|
371
|
-
// fallback scan already used for a chain of test projects.
|
|
372
247
|
const command = nodes.map((n) => n.command).filter(Boolean).join(' && ') || null;
|
|
373
248
|
// A fresh `runs: []` per return — two rollups must never alias one array.
|
|
374
249
|
const nothingRan = (reason) => ({
|
|
@@ -384,49 +259,58 @@ async function runTestsNode({
|
|
|
384
259
|
|
|
385
260
|
if (linux) {
|
|
386
261
|
const { runs, environment } = await runNodesInLinux({
|
|
387
|
-
projectPath, nodes, filter,
|
|
262
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
388
263
|
});
|
|
389
264
|
return rollUpTestRuns(runs, { command, source, environment });
|
|
390
265
|
}
|
|
391
266
|
|
|
392
267
|
const runs = [];
|
|
393
268
|
for (const node of nodes) {
|
|
269
|
+
if (node.e2e && !preDecideRun({ node, filter, projectPath, refreshCache })) {
|
|
270
|
+
stackNodes.push(node);
|
|
271
|
+
runs.push(emptyRunEntry(node));
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
394
274
|
runs.push(await runOneTestNode({
|
|
395
|
-
projectPath, node, filter,
|
|
275
|
+
projectPath, node, filter, timeout, refreshCache, gate, _runTestSuite,
|
|
396
276
|
}));
|
|
397
277
|
}
|
|
398
278
|
|
|
399
279
|
return rollUpTestRuns(runs, { command, source });
|
|
400
280
|
}
|
|
401
281
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
282
|
+
// No stack, no run: the skip carries the e2e verdict as reason AND warning, so the
|
|
283
|
+
// suite never disappears behind a green rollup.
|
|
284
|
+
async function runStackTestNodes({ projectPath, testsNode, stackNodes, e2eNode, stackEnv, timeout, gate, _runTestSuite }) {
|
|
285
|
+
const byId = new Map(stackNodes.map((node) => [node.id, node]));
|
|
286
|
+
const runs = [];
|
|
287
|
+
for (const run of testsNode.runs) {
|
|
288
|
+
const node = byId.get(run.id);
|
|
289
|
+
if (!node) { runs.push(run); continue; }
|
|
290
|
+
if (e2eNode.status !== 'pass') {
|
|
291
|
+
const reason = `a stack do e2e não está de pé (e2e: ${e2eNode.status}${e2eNode.reason ? ` — ${e2eNode.reason}` : ''}): nó e2e: true não roda sem ela`;
|
|
292
|
+
runs.push({ ...run, reason, warnings: [reason] });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
runs.push(await runOneTestNode({ projectPath, node, filter: null, timeout, gate, env: stackEnv, _runTestSuite }));
|
|
296
|
+
}
|
|
297
|
+
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Pre-decisions run before the container exists, so a filtered node is never written
|
|
301
|
+
// into its script; results go through the same `judgeRun()` as the host path.
|
|
417
302
|
async function runNodesInLinux({
|
|
418
|
-
projectPath, nodes, filter,
|
|
303
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
419
304
|
}) {
|
|
420
305
|
const decided = new Map();
|
|
421
306
|
const toRun = [];
|
|
422
307
|
for (const node of nodes) {
|
|
423
|
-
const pre = preDecideRun({ node, filter,
|
|
308
|
+
const pre = preDecideRun({ node, filter, projectPath });
|
|
424
309
|
if (pre) decided.set(node.id, pre);
|
|
425
310
|
else toRun.push(node);
|
|
426
311
|
}
|
|
427
312
|
|
|
428
|
-
// One container, one clock: the
|
|
429
|
-
// have been given locally, since they share the run.
|
|
313
|
+
// One container, one clock: the sum of each node's local timeout.
|
|
430
314
|
const containerTimeout = toRun.reduce(
|
|
431
315
|
(sum, node) => sum + resolveTestTimeout(projectPath, node, timeout), 0
|
|
432
316
|
) || undefined;
|
|
@@ -445,16 +329,13 @@ async function runNodesInLinux({
|
|
|
445
329
|
if (decided.has(node.id)) return decided.get(node.id);
|
|
446
330
|
const raw = byId.get(node.id);
|
|
447
331
|
if (!raw) {
|
|
448
|
-
// The container answered about a fleet that does not include this node.
|
|
449
|
-
// Silence here would be a suite vanishing from the report.
|
|
450
332
|
return { ...emptyRunEntry(node), status: 'fail', reason: 'o container não reportou este nó' };
|
|
451
333
|
}
|
|
452
334
|
if (raw.skipped) {
|
|
453
335
|
return { ...emptyRunEntry(node), reason: raw.reason };
|
|
454
336
|
}
|
|
455
337
|
const result = raw.exitCode === null
|
|
456
|
-
// No exit code is NOT a pass: the container died before this node
|
|
457
|
-
// reported, and a green verdict over an unobserved suite is F30's shape.
|
|
338
|
+
// No exit code is NOT a pass: the container died before this node reported.
|
|
458
339
|
? { passed: false, output: raw.output || '', reason: raw.reason, exitCode: null }
|
|
459
340
|
: { passed: raw.exitCode === 0, output: raw.output || '', exitCode: raw.exitCode };
|
|
460
341
|
return judgeRun({ node, result, durationMs: raw.durationMs ?? 0 });
|
|
@@ -463,36 +344,24 @@ async function runNodesInLinux({
|
|
|
463
344
|
return { runs, environment: suite.environment };
|
|
464
345
|
}
|
|
465
346
|
|
|
466
|
-
/**
|
|
467
|
-
* Execute (or decline to execute) one node, always producing a `runs[]` entry.
|
|
468
|
-
*
|
|
469
|
-
* Nothing is ever dropped: a node that must not run says so with a reason, in
|
|
470
|
-
* the place the reader is already looking. A suite that silently disappears
|
|
471
|
-
* from the report is the defect this whole feature exists to close.
|
|
472
|
-
*/
|
|
473
347
|
async function runOneTestNode({
|
|
474
|
-
projectPath, node, filter,
|
|
348
|
+
projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
|
|
475
349
|
}) {
|
|
476
|
-
const decided = preDecideRun({ node, filter,
|
|
350
|
+
const decided = preDecideRun({ node, filter, projectPath, refreshCache });
|
|
477
351
|
if (decided) return decided;
|
|
478
352
|
|
|
479
|
-
// The eval channel: the CLI DICTATES where the cache, the dataset and the
|
|
480
|
-
// summary live, so the same xunit suite is runnable by `verify` without a
|
|
481
|
-
// second configuration path. `env` is undefined for every other runner, which
|
|
482
|
-
// keeps `runTestSuite` on exactly the path it had before.
|
|
483
353
|
const evalChannel = node.runner === EVAL_RUNNER
|
|
484
354
|
? evalNodeEnv(node, projectPath, { refresh: refreshCache })
|
|
485
355
|
: null;
|
|
486
|
-
// The summary path is fixed per node id and pid
|
|
487
|
-
//
|
|
488
|
-
// process, a recycled pid) was read as THIS run's, handing a harness that
|
|
489
|
-
// wrote nothing the old scenario count — and hiding an empty run behind it.
|
|
356
|
+
// The summary path is fixed per node id and pid: a file left by an earlier run
|
|
357
|
+
// would be read as this run's and hide an empty run behind the old count.
|
|
490
358
|
if (evalChannel) discardFile(evalChannel.summaryPath);
|
|
491
359
|
|
|
492
360
|
gate.enter('tests');
|
|
493
361
|
const start = Date.now();
|
|
362
|
+
const childEnv = env || evalChannel ? { ...(env || {}), ...(evalChannel?.env || {}) } : null;
|
|
494
363
|
const result = await _runTestSuite(projectPath, {
|
|
495
|
-
timeout, node, ...(
|
|
364
|
+
timeout, node, ...(childEnv ? { env: childEnv } : {}),
|
|
496
365
|
});
|
|
497
366
|
const entry = judgeRun({ node, result, durationMs: Date.now() - start });
|
|
498
367
|
if (!evalChannel) return entry;
|
|
@@ -502,26 +371,12 @@ async function runOneTestNode({
|
|
|
502
371
|
return judged;
|
|
503
372
|
}
|
|
504
373
|
|
|
505
|
-
/** Remove a scratch file the CLI owns; a file that is not there (or will not go) changes nothing. */
|
|
506
374
|
function discardFile(path) {
|
|
507
|
-
try { rmSync(path, { force: true }); } catch { /*
|
|
375
|
+
try { rmSync(path, { force: true }); } catch { /* the next run removes it first */ }
|
|
508
376
|
}
|
|
509
377
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
* code said.
|
|
513
|
-
*
|
|
514
|
-
* Two things the exit code cannot see:
|
|
515
|
-
* - the scenario COUNT, which is what `expectedCount` is compared against and
|
|
516
|
-
* what `--record-baseline` writes. Read ONLY from the summary: the `dotnet`
|
|
517
|
-
* extractor counts xunit cases, another unit — compared with (or recorded as)
|
|
518
|
-
* a scenario baseline it failed green runs as drops (#110, #117 review). No
|
|
519
|
-
* readable summary → **null, UNKNOWN, never 0** — an unreadable summary must
|
|
520
|
-
* not manufacture a count drop;
|
|
521
|
-
* - a suite that evaluated ZERO scenarios and exited 0. Nothing failed, because
|
|
522
|
-
* nothing ran: the guardrail-inert shape this repository has already paid to
|
|
523
|
-
* learn to detect.
|
|
524
|
-
*/
|
|
378
|
+
// The scenario count comes ONLY from the summary (the dotnet extractor counts xunit
|
|
379
|
+
// cases, another unit); no readable summary is null, UNKNOWN, never 0.
|
|
525
380
|
function judgeEvalRun(entry, node, summaryPath) {
|
|
526
381
|
const summary = readEvalSummary(summaryPath);
|
|
527
382
|
const warnings = [...(entry.warnings || [])];
|
|
@@ -533,11 +388,8 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
533
388
|
const verdict = compareTestCount(count, node.expectedCount);
|
|
534
389
|
if (verdict.warning) warnings.push(verdict.warning);
|
|
535
390
|
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
// `reason` nunca chegaria ao `feedback[]`; e a cauda do `dotnet` e ruido
|
|
539
|
-
// truncado exatamente onde o sumario ja tem a frase certa. Prepend, e nao
|
|
540
|
-
// substituicao, porque um crash do runner tambem vive nessa cauda.
|
|
391
|
+
// Named failures go into `output` (feedback prefers it to `reason`), prepended
|
|
392
|
+
// rather than replacing it because a runner crash also lives in that tail.
|
|
541
393
|
const named = evalFailureLines(summary);
|
|
542
394
|
const withNames = named.length > 0
|
|
543
395
|
? { ...entry, output: [...named, ...(entry.output ? [entry.output] : [])].join('\n') }
|
|
@@ -556,7 +408,6 @@ function judgeEvalRun(entry, node, summaryPath) {
|
|
|
556
408
|
return { ...withNames, count, warnings };
|
|
557
409
|
}
|
|
558
410
|
|
|
559
|
-
/** The `runs[]` entry every path starts from — every field present, nothing implied. */
|
|
560
411
|
function emptyRunEntry(node) {
|
|
561
412
|
return {
|
|
562
413
|
id: node.id,
|
|
@@ -564,82 +415,41 @@ function emptyRunEntry(node) {
|
|
|
564
415
|
cwd: node.cwd,
|
|
565
416
|
runner: node.runner,
|
|
566
417
|
expectedCount: node.expectedCount,
|
|
567
|
-
//
|
|
568
|
-
// optional chaining. **null is UNKNOWN, never 0** (D7).
|
|
418
|
+
// null is UNKNOWN, never 0.
|
|
569
419
|
count: null,
|
|
570
420
|
status: 'skip',
|
|
571
421
|
durationMs: 0,
|
|
572
422
|
reason: null,
|
|
573
423
|
warnings: [],
|
|
574
424
|
output: '',
|
|
575
|
-
// What the task-scope recorte did to this node (#87): `command` above is
|
|
576
|
-
// what actually ran, `selection.declaredCommand` what was declared. null at
|
|
577
|
-
// feature scope, where applyTaskScope leaves the key ABSENT; absent is read
|
|
578
|
-
// as null here so no reader has to tell the two apart.
|
|
579
|
-
selection: node.selection ?? null,
|
|
580
425
|
};
|
|
581
426
|
}
|
|
582
427
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
*
|
|
587
|
-
* Shared by the local loop and by `--linux`, so a node that must not run is
|
|
588
|
-
* refused for the same reason and with the same words in both environments —
|
|
589
|
-
* two copies of this ordering is how a filtered node ends up invisible in one
|
|
590
|
-
* of them.
|
|
591
|
-
*/
|
|
592
|
-
function preDecideRun({ node, filter, taskId, projectPath = '.', refreshCache = false }) {
|
|
428
|
+
// Verdicts decided before execution, shared by the host loop and `--linux` so both
|
|
429
|
+
// refuse for the same reason with the same words; null means the node must run.
|
|
430
|
+
function preDecideRun({ node, filter, projectPath = '.', refreshCache = false }) {
|
|
593
431
|
const entry = emptyRunEntry(node);
|
|
594
432
|
|
|
595
|
-
// The resolver already decided: invalid config (fail-closed, D4) or an
|
|
596
|
-
// explicit "no suites declared". Executing it would be asking a question that
|
|
597
|
-
// has been answered — and, for an invalid node, answering it wrongly.
|
|
598
433
|
if (node.status) {
|
|
599
434
|
return { ...entry, status: node.status, reason: node.reason };
|
|
600
435
|
}
|
|
601
436
|
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// invalid node costs no clock and still reddens the gate (D4).
|
|
437
|
+
// After the pre-decided verdict on purpose: an invalid node costs no clock and
|
|
438
|
+
// still reddens the gate even when filtered out.
|
|
605
439
|
if (!nodeMatchesFilter(node, filter)) {
|
|
606
440
|
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
607
441
|
}
|
|
608
442
|
|
|
609
|
-
//
|
|
610
|
-
// a node the user filtered out reports the filter, never a scope it was not
|
|
611
|
-
// judged by. BEFORE the e2e and eval checks: a node outside the task scope
|
|
612
|
-
// has nothing left to decide.
|
|
613
|
-
if (node.selection?.mode === 'out-of-scope') {
|
|
614
|
-
return { ...entry, reason: node.selection.reason };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (node.e2e && taskId) {
|
|
618
|
-
return { ...entry, reason: 'e2e node runs at feature scope only' };
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// The eval pre-flight lives HERE, beside the other pre-decisions, so the host
|
|
622
|
-
// loop and the `--linux` path refuse for the same reason with the same words.
|
|
623
|
-
// Its three cache verdicts are FAILURES, never skips: a skip over an unusable
|
|
624
|
-
// cache is a green gate above a verification that never happened. The only
|
|
625
|
-
// skip it produces is scope — an eval suite does not pay for itself inside the
|
|
626
|
-
// per-task loop, and the result Gate 3 reads is the feature-scoped one.
|
|
443
|
+
// A cache miss is a fail, never a skip: a skip would be green over nothing.
|
|
627
444
|
if (node.runner === EVAL_RUNNER) {
|
|
628
|
-
const pre = evalPreflight(projectPath, node, {
|
|
445
|
+
const pre = evalPreflight(projectPath, node, { refresh: refreshCache });
|
|
629
446
|
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
630
447
|
}
|
|
631
448
|
|
|
632
449
|
return null;
|
|
633
450
|
}
|
|
634
451
|
|
|
635
|
-
|
|
636
|
-
* Turn ONE executor result into a `runs[]` entry — the count rule included.
|
|
637
|
-
*
|
|
638
|
-
* Called by both the local loop and the `--linux` container path on purpose:
|
|
639
|
-
* a drop, a rise and an unknown count must mean the same thing whether the
|
|
640
|
-
* suite ran on this host or inside a container. A second copy of this judgement
|
|
641
|
-
* is how two environments start disagreeing about the same suite.
|
|
642
|
-
*/
|
|
452
|
+
// The single count rule for host and container runs; a second copy makes them disagree.
|
|
643
453
|
function judgeRun({ node, result, durationMs }) {
|
|
644
454
|
const entry = emptyRunEntry(node);
|
|
645
455
|
|
|
@@ -647,8 +457,7 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
647
457
|
return { ...entry, durationMs, reason: result.reason || 'no test command detected' };
|
|
648
458
|
}
|
|
649
459
|
|
|
650
|
-
// A
|
|
651
|
-
// a passing run cut mid-sentence. Lead with what actually happened.
|
|
460
|
+
// A truncated log tail reads like a passing run cut mid-sentence: lead with the timeout.
|
|
652
461
|
if (result.timedOut) {
|
|
653
462
|
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
654
463
|
return {
|
|
@@ -661,35 +470,19 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
661
470
|
};
|
|
662
471
|
}
|
|
663
472
|
|
|
664
|
-
//
|
|
665
|
-
// baseline. Read from the FULL output, before it is truncated to a tail —
|
|
666
|
-
// the summary line is the last thing a runner prints, but a red run's tail
|
|
667
|
-
// is chosen for the failures, not for the summary.
|
|
473
|
+
// Parsed from the FULL output, before truncation to a tail.
|
|
668
474
|
const parsed = parseTestCount(result.output, node.runner);
|
|
669
475
|
const count = parsed ? parsed.total : null;
|
|
670
476
|
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
if (entry.selection?.mode === 'narrowed') {
|
|
674
|
-
return judgeNarrowedRun({ entry, node, result, durationMs, count });
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// An eval node's unit is the SCENARIO (#110 part 2): its baseline is the
|
|
678
|
-
// summary's scenario count, and `judgeEvalRun` is the one that compares it.
|
|
679
|
-
// Comparing here too, with the extractor's count of xunit cases, failed a
|
|
680
|
-
// 24/24 run as "esperado 24, executado 2" — and `judgeEvalRun` only turns a
|
|
681
|
-
// pass into a fail, never the reverse.
|
|
477
|
+
// An eval node's unit is the SCENARIO, compared only by `judgeEvalRun`; the
|
|
478
|
+
// extractor's xunit-case count against that baseline would fail green runs.
|
|
682
479
|
const verdict = compareTestCount(count, node.runner === EVAL_RUNNER ? null : node.expectedCount);
|
|
683
480
|
const warnings = verdict.warning ? [verdict.warning] : [];
|
|
684
481
|
|
|
685
|
-
//
|
|
686
|
-
// used to run no longer do. A RISE never fails (D7) — the suite grows with
|
|
687
|
-
// every task, and failing on growth teaches deleting tests.
|
|
482
|
+
// Only a drop fails a node; a rise never does, or failing on growth teaches deleting tests.
|
|
688
483
|
if (verdict.verdict === 'drop' && result.passed) {
|
|
689
484
|
return { ...entry, status: 'fail', durationMs, count, warnings, reason: verdict.reason, output: '' };
|
|
690
485
|
}
|
|
691
|
-
// Already red: the failing test is the better lead, so the drop travels
|
|
692
|
-
// beside it instead of overwriting the reason.
|
|
693
486
|
if (verdict.verdict === 'drop') warnings.push(verdict.reason);
|
|
694
487
|
|
|
695
488
|
return {
|
|
@@ -698,85 +491,13 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
698
491
|
durationMs,
|
|
699
492
|
count,
|
|
700
493
|
warnings,
|
|
701
|
-
// `test-build-failed` / `stale-binary`:
|
|
702
|
-
// test ran, and the reason is the only thing worth reading. Without this
|
|
703
|
-
// the node would go red with a null reason and the reader would go hunting
|
|
704
|
-
// through a compiler log for a suite that never started.
|
|
494
|
+
// `test-build-failed` / `stale-binary`: refused before any test ran.
|
|
705
495
|
reason: result.reason ?? null,
|
|
706
496
|
output: result.passed ? '' : tailLines(result.output, OUTPUT_TAIL_LINES),
|
|
707
497
|
};
|
|
708
498
|
}
|
|
709
499
|
|
|
710
|
-
|
|
711
|
-
* The verdict of a NARROWED run (#87): what `judgeRun` hands over instead of
|
|
712
|
-
* its count rule. Reached from the host loop and from `--linux` alike, because
|
|
713
|
-
* both go through `judgeRun`.
|
|
714
|
-
*
|
|
715
|
-
* Two things change, and both follow from "partial by construction":
|
|
716
|
-
*
|
|
717
|
-
* - NO `compareTestCount`. `expectedCount` is the WHOLE suite's baseline: a
|
|
718
|
-
* recorte running 12 of 3.082 is not a drop, and a superset selection
|
|
719
|
-
* running more than the baseline is not a rise. Either comparison would
|
|
720
|
-
* report something about the suite that this run never measured.
|
|
721
|
-
* - a pass needs PROOF that tests ran. A filter that matches nothing exits 0
|
|
722
|
-
* under VSTest, and "zero tests, all green" is exactly the verdict issue #87
|
|
723
|
-
* must never buy. What counts as proof is the selection's `zeroGuard`
|
|
724
|
-
* (test-scope.js):
|
|
725
|
-
* `count` only a parsed count > 0. **null is UNKNOWN, never green**;
|
|
726
|
-
* `mtp-exit-8` a parsed count > 0 proves it (the MTP summary is read in
|
|
727
|
-
* pt-BR and en-US, #115); with no readable summary (another
|
|
728
|
-
* locale), exit 0 does — MTP exits 8 on zero tests.
|
|
729
|
-
* A parsed 0 is refused under either guard: it is positive evidence. It is
|
|
730
|
-
* also what closes limit (f): with exit 8 IGNORED, MTP exits 0 over a
|
|
731
|
-
* summary that says `total: 0` (measured, mtp-ignore-exit-code-8-zero-tests.txt).
|
|
732
|
-
* In a narrowed `&&` chain the SUM would hide that link behind another's
|
|
733
|
-
* count, so under `mtp-exit-8` every MTP block is checked on its own — MTP
|
|
734
|
-
* prints one block per invocation (measured), and a `total: 0` block is red.
|
|
735
|
-
*
|
|
736
|
-
* A FAILED run is renamed `narrowed-zero-tests (…)` only when it carries the
|
|
737
|
-
* runner's zero-test signature AND no count proves tests ran. A multi-project
|
|
738
|
-
* VSTest output may print "No test matches…" for one project while another
|
|
739
|
-
* ran and failed; that failure is the lead, never a zero. Anything else,
|
|
740
|
-
* exit 9 included, keeps the executor's own raw reason: a project that imposes
|
|
741
|
-
* `--minimum-expected-tests` is narrowed only with its floor lowered to 1 by
|
|
742
|
-
* `-p:` (#114, test-scope.js), where a run with tests cannot exit 9 — so a 9
|
|
743
|
-
* here is not ours to reinterpret.
|
|
744
|
-
*
|
|
745
|
-
* Every red leads its output with the verdict, like a timeout does: the
|
|
746
|
-
* feedback line reads `output` before `reason`, and a tail alone would bury it.
|
|
747
|
-
*/
|
|
748
|
-
function judgeNarrowedRun({ entry, node, result, durationMs, count }) {
|
|
749
|
-
const tail = tailLines(result.output, OUTPUT_TAIL_LINES);
|
|
750
|
-
const base = { ...entry, durationMs, count, warnings: [] };
|
|
751
|
-
const red = (reason) => ({ ...base, status: 'fail', reason, output: tail ? `${reason}\n${tail}` : reason });
|
|
752
|
-
const green = () => ({ ...base, status: 'pass', reason: result.reason ?? null, output: '' });
|
|
753
|
-
const ranTests = Number.isInteger(count) && count > 0;
|
|
754
|
-
|
|
755
|
-
if (result.passed) {
|
|
756
|
-
if (count === 0) return red(NARROWED_ZERO_TESTS);
|
|
757
|
-
if (entry.selection.zeroGuard === 'mtp-exit-8' && mtpSummaryTotals(result.output).includes(0)) {
|
|
758
|
-
return red(NARROWED_ZERO_TESTS_LINK);
|
|
759
|
-
}
|
|
760
|
-
if (ranTests) return green();
|
|
761
|
-
if (entry.selection.zeroGuard === 'mtp-exit-8' && result.exitCode === 0) return green();
|
|
762
|
-
return red(NARROWED_COUNT_UNKNOWN);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (!ranTests && isZeroTestRun(result.output, node.runner, result.exitCode)) {
|
|
766
|
-
return red(`narrowed-zero-tests (${node.runner}, exit ${result.exitCode ?? 'desconhecido'}): o recorte executou 0 testes`);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
return { ...base, status: 'fail', reason: result.reason ?? null, output: tail };
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Aggregate the per-node runs into the single node `verify` publishes.
|
|
774
|
-
*
|
|
775
|
-
* The rollup is pessimistic on purpose: ANY red run reddens it. A green
|
|
776
|
-
* aggregate covering a red suite is F30 restated, so `some(fail)` is checked
|
|
777
|
-
* before anything else. `skip` only survives when there is nothing else to
|
|
778
|
-
* report — one green run among skips is a pass.
|
|
779
|
-
*/
|
|
500
|
+
// Any red run reddens the rollup; one green run among skips is a pass.
|
|
780
501
|
function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
781
502
|
const failed = runs.filter((r) => r.status === 'fail');
|
|
782
503
|
const status = failed.length > 0
|
|
@@ -790,13 +511,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
790
511
|
reason: null,
|
|
791
512
|
output: failed.map((r) => r.output).filter(Boolean).join('\n'),
|
|
792
513
|
source,
|
|
793
|
-
//
|
|
794
|
-
// by name and image, because a green from a container and a green from the
|
|
795
|
-
// host are otherwise indistinguishable in feature.json — and the next
|
|
796
|
-
// person would not know what was proven.
|
|
514
|
+
// null is the local host; a container run is named so feature.json says what was proven.
|
|
797
515
|
environment,
|
|
798
|
-
// Named by suite: a warning that does not say WHICH baseline went stale
|
|
799
|
-
// sends the reader to read every one of them.
|
|
800
516
|
warnings: runs.flatMap((r) => (r.warnings || []).map((w) => `${r.id}: ${w}`)),
|
|
801
517
|
runs,
|
|
802
518
|
};
|
|
@@ -810,17 +526,8 @@ function rollUpTestRuns(runs, { command, source, environment = null }) {
|
|
|
810
526
|
return node;
|
|
811
527
|
}
|
|
812
528
|
|
|
813
|
-
/**
|
|
814
|
-
* One feedback line per RED node, named by node id.
|
|
815
|
-
*
|
|
816
|
-
* The `[tests/{id}]` prefix replaces the old bare `[tests]`: with N suites, a
|
|
817
|
-
* line that does not say which one failed sends the reader to the wrong repo
|
|
818
|
-
* half.
|
|
819
|
-
*/
|
|
820
529
|
function appendTestsFeedback(feedback, testsNode) {
|
|
821
|
-
// Warnings
|
|
822
|
-
// GREEN node by construction (the count rose), so a warning that only
|
|
823
|
-
// printed on failure would never print at all.
|
|
530
|
+
// Warnings print whatever the verdict: a stale baseline lives on a GREEN node.
|
|
824
531
|
for (const run of testsNode.runs || []) {
|
|
825
532
|
for (const warning of run.warnings || []) {
|
|
826
533
|
feedback.push(`[tests/${run.id}] warning: ${warning}`);
|
|
@@ -835,31 +542,7 @@ function appendTestsFeedback(feedback, testsNode) {
|
|
|
835
542
|
}
|
|
836
543
|
}
|
|
837
544
|
|
|
838
|
-
|
|
839
|
-
* Why `--refresh-cache` cannot mean what it says, or null when it can.
|
|
840
|
-
*
|
|
841
|
-
* Judged BEFORE the build node, and the ordering is the whole point: a refusal
|
|
842
|
-
* that arrived after the cache was touched would leave a half-rewritten tree
|
|
843
|
-
* behind. Same discipline `finish --merge` learned — validate the precondition
|
|
844
|
-
* before the mutation, never after.
|
|
845
|
-
*
|
|
846
|
-
* Three refusals, all usage errors (exit 2), never verdicts:
|
|
847
|
-
* - no eval node declared: succeeding silently would teach the user to type a
|
|
848
|
-
* flag that does nothing;
|
|
849
|
-
* - `--skip-tests`: the two flags cancel each other, and the winner would be
|
|
850
|
-
* whichever the code happened to check first;
|
|
851
|
-
* - a filter that excludes every eval node: same shape as the first.
|
|
852
|
-
*
|
|
853
|
-
* The MISSING CREDENTIAL is deliberately NOT judged here: it is per node (each
|
|
854
|
-
* declares its own `envFile`), so `evalPreflight` answers it — still before the
|
|
855
|
-
* command runs, and still without touching the tree.
|
|
856
|
-
*
|
|
857
|
-
* @param {string} projectPath
|
|
858
|
-
* @param {{nodes: Array}} testPlan
|
|
859
|
-
* @param {{tokens: string[], raw: string}|null} filter
|
|
860
|
-
* @param {{skipTests: boolean}} opts
|
|
861
|
-
* @returns {string|null}
|
|
862
|
-
*/
|
|
545
|
+
// The missing credential is judged per node by `evalPreflight`, not here.
|
|
863
546
|
function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
864
547
|
if (skipTests) {
|
|
865
548
|
return '--refresh-cache com --skip-tests: as duas flags se anulam — a regravação da cache É a suíte de eval';
|
|
@@ -879,41 +562,9 @@ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
|
879
562
|
return null;
|
|
880
563
|
}
|
|
881
564
|
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Why `--record-baseline` cannot run here, or null when it can.
|
|
888
|
-
*
|
|
889
|
-
* Judged BEFORE the build node, beside the `--filter` check, because both are
|
|
890
|
-
* statements about a command that cannot mean what it says. Paying for a full
|
|
891
|
-
* suite to be told there was nowhere to write the result is the worst possible
|
|
892
|
-
* ordering.
|
|
893
|
-
*
|
|
894
|
-
* The refusal for a project with no `project.tests[]` is deliberate rather
|
|
895
|
-
* than helpful: the nodes the fallback synthesizes are not declared anywhere,
|
|
896
|
-
* so there is no entry to update, and writing one would mean this command
|
|
897
|
-
* inventing the declaration it came to maintain. `config.json` is likewise
|
|
898
|
-
* never created — the framework repository has none on purpose (creating it
|
|
899
|
-
* breaks `doctor --full`), so the honest answer is a named usage error.
|
|
900
|
-
*
|
|
901
|
-
* A TASK id is refused first (#87): at task scope the runner narrows the nodes
|
|
902
|
-
* AND the commands, so what it measures is a recorte, while `expectedCount` is
|
|
903
|
-
* the whole fleet's number. `recordTestBaselines` refuses a measurement that
|
|
904
|
-
* carries a selection (the second barrier), but paying for the recorte to
|
|
905
|
-
* learn that is the ordering this function exists to prevent.
|
|
906
|
-
*
|
|
907
|
-
* @param {string} projectPath
|
|
908
|
-
* @param {{ nodes: Array, source: string }} testPlan
|
|
909
|
-
* @param {{ skipTests: boolean, taskId?: string|null }} opts
|
|
910
|
-
* @returns {string|null}
|
|
911
|
-
*/
|
|
912
|
-
function baselineTargetProblem(projectPath, testPlan, { skipTests, taskId = null }) {
|
|
913
|
-
if (taskId) {
|
|
914
|
-
return `--record-baseline é manutenção da frota: rode sem {task} (recebeu "${taskId}") — `
|
|
915
|
-
+ 'no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline';
|
|
916
|
-
}
|
|
565
|
+
// Never creates config.json nor a `project.tests[]` entry: fallback nodes have no
|
|
566
|
+
// declaration to update, and inventing one is not this command's job.
|
|
567
|
+
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
917
568
|
if (skipTests) {
|
|
918
569
|
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
919
570
|
}
|
|
@@ -928,20 +579,8 @@ function baselineTargetProblem(projectPath, testPlan, { skipTests, taskId = null
|
|
|
928
579
|
return null;
|
|
929
580
|
}
|
|
930
581
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
*
|
|
934
|
-
* D8 lives here: the number is only ever what a REAL run measured, and only
|
|
935
|
-
* when every node of that run is green. A red run records nothing — writing a
|
|
936
|
-
* lower count over a regression is how the regression becomes the new normal.
|
|
937
|
-
* A PARTIAL run records nothing either: `--filter` and skips leave nodes
|
|
938
|
-
* unmeasured, and a baseline covering half the fleet is a claim about the
|
|
939
|
-
* other half that nobody made.
|
|
940
|
-
*
|
|
941
|
-
* @param {string} projectPath
|
|
942
|
-
* @param {Object} testsNode - The rollup produced by runTestsNode()
|
|
943
|
-
* @returns {{ recorded: boolean, reason: string|null, updates: Array, path?: string }}
|
|
944
|
-
*/
|
|
582
|
+
// Only a run with EVERY node green records: a red run would freeze the regression,
|
|
583
|
+
// a partial one would claim counts for nodes nobody measured.
|
|
945
584
|
function recordBaselineFrom(projectPath, testsNode) {
|
|
946
585
|
const runs = testsNode.runs || [];
|
|
947
586
|
const refuse = (reason) => ({ recorded: false, reason, updates: [] });
|
|
@@ -957,24 +596,13 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
957
596
|
);
|
|
958
597
|
}
|
|
959
598
|
|
|
960
|
-
|
|
961
|
-
// (#87) — the second barrier behind baselineTargetProblem's usage error.
|
|
962
|
-
return recordTestBaselines(
|
|
963
|
-
projectPath,
|
|
964
|
-
runs.map((r) => ({ id: r.id, count: r.count, selection: r.selection ?? null }))
|
|
965
|
-
);
|
|
599
|
+
return recordTestBaselines(projectPath, runs.map((r) => ({ id: r.id, count: r.count })));
|
|
966
600
|
}
|
|
967
601
|
|
|
968
|
-
// ============================================================================
|
|
969
|
-
// Validators node
|
|
970
|
-
// ============================================================================
|
|
971
|
-
|
|
972
602
|
async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
973
603
|
const start = Date.now();
|
|
974
604
|
|
|
975
|
-
// runValidation() prints
|
|
976
|
-
// JSON — silence console during the call and ALWAYS restore it, even if
|
|
977
|
-
// runValidation throws.
|
|
605
|
+
// runValidation() prints progress to stdout, which would corrupt verify's JSON.
|
|
978
606
|
const originalLog = console.log;
|
|
979
607
|
const originalWarn = console.warn;
|
|
980
608
|
let validationResult;
|
|
@@ -1013,8 +641,7 @@ async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
|
1013
641
|
}
|
|
1014
642
|
|
|
1015
643
|
function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
1016
|
-
// A validator that could not check
|
|
1017
|
-
// green node; when the whole node skipped, its own line already says why.
|
|
644
|
+
// A validator that could not check is invisible in a green node otherwise.
|
|
1018
645
|
if (validatorsNode.status !== 'skip') {
|
|
1019
646
|
for (const s of validatorsNode.skipped || []) {
|
|
1020
647
|
if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
|
|
@@ -1031,57 +658,51 @@ function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
|
1031
658
|
}
|
|
1032
659
|
}
|
|
1033
660
|
|
|
1034
|
-
|
|
1035
|
-
// E2E node
|
|
1036
|
-
// ============================================================================
|
|
1037
|
-
|
|
1038
|
-
async function runE2eNode({ projectPath, featureName, taskId, task = null, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
|
|
661
|
+
async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
|
|
1039
662
|
if (buildNode.status === 'fail') {
|
|
1040
663
|
return {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
664
|
+
node: {
|
|
665
|
+
status: 'skip',
|
|
666
|
+
command: null,
|
|
667
|
+
durationMs: 0,
|
|
668
|
+
reason: 'build failed — the stack could not come up anyway',
|
|
669
|
+
output: '',
|
|
670
|
+
},
|
|
671
|
+
stackEnv: null,
|
|
1046
672
|
};
|
|
1047
673
|
}
|
|
1048
674
|
|
|
1049
675
|
const config = readConfig(projectPath);
|
|
1050
|
-
const taskGroup = task?.group ?? null;
|
|
1051
676
|
const gitCommonDir = resolveGitCommonDir(projectPath);
|
|
677
|
+
// Without the port block, two worktrees verifying at once collide on host ports.
|
|
678
|
+
const env = buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir });
|
|
1052
679
|
|
|
1053
680
|
const start = Date.now();
|
|
1054
681
|
const result = _runE2eCheck(projectPath, {
|
|
1055
682
|
config,
|
|
1056
683
|
featureName,
|
|
1057
|
-
taskGroup,
|
|
1058
|
-
scope: taskId ? 'task' : 'feature',
|
|
1059
684
|
gitCommonDir,
|
|
1060
|
-
|
|
1061
|
-
// runs in the loop, and two worktrees verifying at once would otherwise get
|
|
1062
|
-
// distinct container names and then collide on host port 3000 — a red gate
|
|
1063
|
-
// over an infrastructure clash instead of a defect.
|
|
1064
|
-
env: buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir }),
|
|
685
|
+
env,
|
|
1065
686
|
skipRequested: skipE2e,
|
|
1066
687
|
forceFresh: freshE2e,
|
|
1067
688
|
beforeHeavy: () => gate.enter('e2e'),
|
|
1068
|
-
|
|
1069
|
-
// container per
|
|
1070
|
-
// nobody runs. `morph-spec e2e down` (and `finish`) tear it down.
|
|
689
|
+
countSpecs: (output) => parseTestCount(output, 'playwright')?.total ?? null,
|
|
690
|
+
// Cold-starting a .NET container per verify is too slow; `e2e down` and `finish` tear it down.
|
|
1071
691
|
keepUp: true,
|
|
1072
692
|
});
|
|
1073
693
|
const durationMs = Date.now() - start;
|
|
1074
694
|
|
|
1075
695
|
return {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
696
|
+
node: {
|
|
697
|
+
status: result.status,
|
|
698
|
+
command: result.command ?? null,
|
|
699
|
+
durationMs,
|
|
700
|
+
reason: result.reason ?? null,
|
|
701
|
+
output: result.output || '',
|
|
702
|
+
count: Number.isInteger(result.count) ? result.count : null,
|
|
703
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
704
|
+
},
|
|
705
|
+
stackEnv: { COMPOSE_PROJECT_NAME: composeProjectName(featureName), ...env },
|
|
1085
706
|
};
|
|
1086
707
|
}
|
|
1087
708
|
|
|
@@ -1096,41 +717,8 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1096
717
|
}
|
|
1097
718
|
}
|
|
1098
719
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
*
|
|
1102
|
-
* The ONE reader of tasks.json for verify: the group picks the e2e
|
|
1103
|
-
* applicability, group + outputs pick the test scope, and the command uses the
|
|
1104
|
-
* null to refuse an unknown task id.
|
|
1105
|
-
*
|
|
1106
|
-
* @param {string} projectPath
|
|
1107
|
-
* @param {string} featureName
|
|
1108
|
-
* @param {string} taskId
|
|
1109
|
-
* @returns {{ group: string|null, outputs: string[] }|null}
|
|
1110
|
-
*/
|
|
1111
|
-
export function readTask(projectPath, featureName, taskId) {
|
|
1112
|
-
try {
|
|
1113
|
-
const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
|
|
1114
|
-
if (!existsSync(tasksPath)) return null;
|
|
1115
|
-
const tasksJson = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
1116
|
-
const task = (tasksJson.tasks || []).find((t) => t.id === taskId);
|
|
1117
|
-
if (!task) return null;
|
|
1118
|
-
return {
|
|
1119
|
-
group: task.group ?? null,
|
|
1120
|
-
outputs: Array.isArray(task.outputs) ? task.outputs : [],
|
|
1121
|
-
};
|
|
1122
|
-
} catch {
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// ============================================================================
|
|
1128
|
-
// Shared helpers
|
|
1129
|
-
// ============================================================================
|
|
1130
|
-
|
|
1131
|
-
/** Read the project stack directly from config.json — never the stack-profile
|
|
1132
|
-
* default (which falls back to 'unknown'→dotnet and would wrongly fire
|
|
1133
|
-
* `dotnet build` on a greenfield JS project). */
|
|
720
|
+
// Never the stack-profile default: it falls back to dotnet and would fire
|
|
721
|
+
// `dotnet build` on a greenfield JS project.
|
|
1134
722
|
function readStack(projectPath) {
|
|
1135
723
|
try {
|
|
1136
724
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1142,10 +730,6 @@ function readStack(projectPath) {
|
|
|
1142
730
|
}
|
|
1143
731
|
}
|
|
1144
732
|
|
|
1145
|
-
/** Read frontend/backend sub-paths for a monorepo, so the build node can run
|
|
1146
|
-
* each stack's build command from its own directory instead of assuming a
|
|
1147
|
-
* single root (see getBuildSteps() in build-check.js). Absent in single-stack
|
|
1148
|
-
* projects — every consumer treats missing paths as "use projectPath". */
|
|
1149
733
|
function readProjectPaths(projectPath) {
|
|
1150
734
|
try {
|
|
1151
735
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1158,7 +742,6 @@ function readProjectPaths(projectPath) {
|
|
|
1158
742
|
}
|
|
1159
743
|
}
|
|
1160
744
|
|
|
1161
|
-
/** Read the whole config.json — the e2e node needs `e2e` and `project` together. */
|
|
1162
745
|
function readConfig(projectPath) {
|
|
1163
746
|
try {
|
|
1164
747
|
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
@@ -1169,14 +752,8 @@ function readConfig(projectPath) {
|
|
|
1169
752
|
}
|
|
1170
753
|
}
|
|
1171
754
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
* location and rationale as the fleet env-registry (see env-allocator.js): it
|
|
1175
|
-
* is the one place every linked worktree of a repo shares, and writing there
|
|
1176
|
-
* can never dirty the `git status` the worktree-finish gates read.
|
|
1177
|
-
*
|
|
1178
|
-
* Fail-open: no git, no stamp — the cost is a rebuild, never a crash.
|
|
1179
|
-
*/
|
|
755
|
+
// The e2e freshness stamp lives in the common dir: shared by every worktree and
|
|
756
|
+
// never dirties `git status`. Fail-open: no git costs a rebuild, never a crash.
|
|
1180
757
|
function resolveGitCommonDir(projectPath) {
|
|
1181
758
|
try {
|
|
1182
759
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|