@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,34 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test Runner — the FALLBACK resolver and the executor of a single test node.
|
|
3
|
-
*
|
|
4
|
-
* Scope note: a project has a LIST of test suites, and that list is resolved by
|
|
5
|
-
* `test-nodes.js` (`resolveTestNodes()`), which calls `resolveTestPlan()` below
|
|
6
|
-
* as its last fallback. What lives here is (a) the four-step guess for a
|
|
7
|
-
* project that declared nothing, and (b) `runTestSuite()`, which runs ONE node.
|
|
8
|
-
* The plural/singular split matters: a runner that resolved its own single
|
|
9
|
-
* command is what let a whole second suite sit outside the gate (defect F30).
|
|
10
|
-
*
|
|
11
|
-
* Resolution order for the fallback test command:
|
|
12
|
-
* 1. .morph/config/config.json → project.testCommand
|
|
13
|
-
* 2. .sln or .csproj present at project root → "dotnet test"
|
|
14
|
-
* 3. Nested *Tests.csproj anywhere in the tree (monorepo without a root
|
|
15
|
-
* .sln) → "dotnet test <proj> -v q" for each match
|
|
16
|
-
* 4. package.json present AND has scripts.test → "npm test"
|
|
17
|
-
* 5. null (no test suite detected)
|
|
18
|
-
*
|
|
19
|
-
* Step 3 deliberately does NOT see linked git worktrees. A worktree is another
|
|
20
|
-
* feature's checkout of the SAME repo: its `tests/X.Tests.csproj` is a copy of
|
|
21
|
-
* the one already matched at the root, so including it both doubles the wall
|
|
22
|
-
* clock and reports on code that is not the tree under verification. In the
|
|
23
|
-
* field this produced a red `tests` node — "fail" at exactly the timeout, with
|
|
24
|
-
* the first run's own summary reading "Com falha: 0" — while nothing had
|
|
25
|
-
* failed. Two independent filters carry that rule (see collectDotnetTestProjects):
|
|
26
|
-
* the CONVENTION path (worktrees/, .worktrees/) and, for worktrees created
|
|
27
|
-
* anywhere else, the registry git itself keeps.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
1
|
import { execSync, execFileSync } from 'child_process';
|
|
31
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'fs';
|
|
32
3
|
import { join, resolve, relative, isAbsolute } from 'path';
|
|
33
4
|
import { globSync } from 'glob';
|
|
34
5
|
|
|
@@ -36,30 +7,20 @@ import { WORKTREES_DIR, LEGACY_WORKTREES_DIR } from '../../commands/project/work
|
|
|
36
7
|
import { getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
37
8
|
import { CONFIG_FILE_NAME } from '../../core/state/state-manager.js';
|
|
38
9
|
|
|
39
|
-
|
|
40
|
-
* Timeout budget granted to ONE test project. The chained `a && b` command is
|
|
41
|
-
* given this much per link: a fixed cap made every multi-project chain born
|
|
42
|
-
* condemned — it timed out for being long, not for being broken.
|
|
43
|
-
*/
|
|
10
|
+
// Per chained project: a fixed cap timed a long `a && b` chain out for being long, not broken.
|
|
44
11
|
export const DEFAULT_TEST_TIMEOUT_PER_PROJECT = 120_000;
|
|
45
12
|
|
|
46
13
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
51
|
-
* @returns {{ command: string | null, projectCount: number, source: string }}
|
|
52
|
-
* `source` is one of 'config' | 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'.
|
|
14
|
+
* Fallback test command when the project declared no nodes; `projectCount` scales the timeout.
|
|
15
|
+
* `source`: 'config' | 'dotnet-root' | 'dotnet-scan' | 'npm' | 'none'.
|
|
53
16
|
*/
|
|
54
17
|
export function resolveTestPlan(projectPath = '.') {
|
|
55
|
-
// 1. Read from .morph/config/config.json
|
|
56
18
|
const config = readMorphConfig(projectPath);
|
|
57
19
|
const configured = config?.project?.testCommand;
|
|
58
20
|
if (configured && typeof configured === 'string') {
|
|
59
21
|
return { command: configured, projectCount: 1, source: 'config' };
|
|
60
22
|
}
|
|
61
23
|
|
|
62
|
-
// 2. Detect .NET project (sln/csproj at root) → dotnet test
|
|
63
24
|
try {
|
|
64
25
|
const entries = readdirSync(projectPath);
|
|
65
26
|
const hasDotnetProject = entries.some(
|
|
@@ -69,40 +30,19 @@ export function resolveTestPlan(projectPath = '.') {
|
|
|
69
30
|
return { command: 'dotnet test', projectCount: 1, source: 'dotnet-root' };
|
|
70
31
|
}
|
|
71
32
|
} catch {
|
|
72
|
-
// Unreadable project directory — fall through to next detection
|
|
73
33
|
}
|
|
74
34
|
|
|
75
|
-
// 3. No root-level project file — look for a nested test project
|
|
76
|
-
// (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
|
|
77
|
-
// have no root .sln. Run each matched project explicitly rather than a
|
|
78
|
-
// bare `dotnet test` (which finds nothing to build at the root).
|
|
79
35
|
const testProjects = collectDotnetTestProjects(projectPath);
|
|
80
36
|
if (testProjects.length > 0) {
|
|
81
37
|
return {
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// Microsoft.Testing.Platform (`"test": {"runner":
|
|
85
|
-
// "Microsoft.Testing.Platform"}` in global.json — what xunit.v3 4.x on
|
|
86
|
-
// .NET 10 requires, and what the ai-agents standards prescribe) runs
|
|
87
|
-
// `dotnet test` in MTP MODE, where `--nologo` is unknown and is forwarded
|
|
88
|
-
// verbatim to the test application. The application answers
|
|
89
|
-
// `error: unknown option: --nologo` and the session dies with MTP exit
|
|
90
|
-
// code 5 (InvalidCommandLine) reporting ZERO tests — a red `tests` node
|
|
91
|
-
// that looks exactly like a broken suite while every test passes.
|
|
92
|
-
// Measured on framework/templates/dotnet/ai-kit (2026-09-08): with the
|
|
93
|
-
// flag, "Zero testes executados" / exit 5; without it, 115 passed / exit 0.
|
|
94
|
-
// The rest of the command is mode-agnostic on purpose: the POSITIONAL
|
|
95
|
-
// project path and `-v q` are accepted by both modes, so this command
|
|
96
|
-
// stays correct whether the target project is VSTest or MTP. Do not
|
|
97
|
-
// "modernize" the path to `--project`: that spelling exists only in MTP
|
|
98
|
-
// mode and would break every VSTest project instead.
|
|
38
|
+
// No `--nologo` (VSTest-only: MTP forwards it and dies with exit 5, zero tests)
|
|
39
|
+
// and no `--project` (MTP-only): positional path and `-v q` work in both modes.
|
|
99
40
|
command: testProjects.map((p) => `dotnet test "${p}" -v q`).join(' && '),
|
|
100
41
|
projectCount: testProjects.length,
|
|
101
42
|
source: 'dotnet-scan',
|
|
102
43
|
};
|
|
103
44
|
}
|
|
104
45
|
|
|
105
|
-
// 4. package.json present AND has scripts.test → npm test
|
|
106
46
|
const packageJsonPath = join(projectPath, 'package.json');
|
|
107
47
|
if (existsSync(packageJsonPath)) {
|
|
108
48
|
try {
|
|
@@ -111,29 +51,15 @@ export function resolveTestPlan(projectPath = '.') {
|
|
|
111
51
|
return { command: 'npm test', projectCount: 1, source: 'npm' };
|
|
112
52
|
}
|
|
113
53
|
} catch {
|
|
114
|
-
// Malformed package.json — fall through to null
|
|
115
54
|
}
|
|
116
55
|
}
|
|
117
56
|
|
|
118
|
-
// 5. Nothing found
|
|
119
57
|
return { command: null, projectCount: 0, source: 'none' };
|
|
120
58
|
}
|
|
121
59
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
126
|
-
* Two filters, because one is not enough:
|
|
127
|
-
* - the glob ignore covers worktrees created by convention (`worktrees/{f}`
|
|
128
|
-
* and the legacy `.worktrees/{f}`), which is where `morph-spec worktree
|
|
129
|
-
* setup` puts them;
|
|
130
|
-
* - `git worktree list --porcelain` covers every OTHER location, including a
|
|
131
|
-
* worktree someone created by hand outside the convention. Fail-open: no
|
|
132
|
-
* git, no answer, no filter — the convention filter still stands.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} projectPath
|
|
135
|
-
* @returns {string[]} Project-relative csproj paths, sorted
|
|
136
|
-
*/
|
|
60
|
+
// A linked worktree is another feature's checkout: scanning it doubles the clock and
|
|
61
|
+
// reports on code outside the tree. The glob ignore covers the convention dirs;
|
|
62
|
+
// `git worktree list` covers worktrees created anywhere else (fail-open).
|
|
137
63
|
function collectDotnetTestProjects(projectPath) {
|
|
138
64
|
let matches;
|
|
139
65
|
try {
|
|
@@ -148,7 +74,6 @@ function collectDotnetTestProjects(projectPath) {
|
|
|
148
74
|
],
|
|
149
75
|
});
|
|
150
76
|
} catch {
|
|
151
|
-
// Glob failure — caller falls through to the next detection step
|
|
152
77
|
return [];
|
|
153
78
|
}
|
|
154
79
|
|
|
@@ -162,20 +87,8 @@ function collectDotnetTestProjects(projectPath) {
|
|
|
162
87
|
return matches.sort();
|
|
163
88
|
}
|
|
164
89
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* under verification may be addressed by its 8.3 SHORT form (`MORPHT~1` —
|
|
168
|
-
* every GitHub Windows runner's temp dir, and any user path with a short
|
|
169
|
-
* name). `path.resolve` does not expand aliases, so `isInside(long, short)`
|
|
170
|
-
* says "not inside" for the same directory and a linked worktree leaks into
|
|
171
|
-
* the test command. Only `realpathSync.native` asks the OS. Fail-open to the
|
|
172
|
-
* plain resolve for a path that does not exist (yet).
|
|
173
|
-
*
|
|
174
|
-
* Exported because test-scope.js spells every task output with it (#87 F4): a
|
|
175
|
-
* mis-cased output resolves on a case-insensitive disk and was forwarded to the
|
|
176
|
-
* runner verbatim — and the `--linux` container is case-sensitive. The OS also
|
|
177
|
-
* returns the real CASE of each component, so this one call is both answers.
|
|
178
|
-
*/
|
|
90
|
+
// Git prints the long path; the tree may be addressed by its 8.3 short form (`MORPHT~1`).
|
|
91
|
+
// `path.resolve` does not expand that alias, only `realpathSync.native` does.
|
|
179
92
|
export function canonicalPath(p) {
|
|
180
93
|
try {
|
|
181
94
|
return realpathSync.native(p);
|
|
@@ -184,14 +97,6 @@ export function canonicalPath(p) {
|
|
|
184
97
|
}
|
|
185
98
|
}
|
|
186
99
|
|
|
187
|
-
/**
|
|
188
|
-
* Absolute paths of the git worktrees linked to this repo, excluding the tree
|
|
189
|
-
* being verified. Fail-open — a repo without git (or a git that errors) yields
|
|
190
|
-
* an empty list, and discovery proceeds on the convention filter alone.
|
|
191
|
-
*
|
|
192
|
-
* @param {string} projectPath
|
|
193
|
-
* @returns {string[]}
|
|
194
|
-
*/
|
|
195
100
|
function listLinkedWorktreePaths(projectPath) {
|
|
196
101
|
let out;
|
|
197
102
|
try {
|
|
@@ -213,61 +118,24 @@ function listLinkedWorktreePaths(projectPath) {
|
|
|
213
118
|
.filter((p) => p && p !== self);
|
|
214
119
|
}
|
|
215
120
|
|
|
216
|
-
/**
|
|
217
|
-
* True when `child` is `parent` or lives under it.
|
|
218
|
-
*
|
|
219
|
-
* Exported because test-nodes.js asks the SAME question of a declared `cwd`
|
|
220
|
-
* ("does this node point outside the tree under verification?"). One spelling
|
|
221
|
-
* of containment, one place to fix it.
|
|
222
|
-
*
|
|
223
|
-
* @param {string} child
|
|
224
|
-
* @param {string} parent
|
|
225
|
-
* @returns {boolean}
|
|
226
|
-
*/
|
|
121
|
+
/** True when `child` is `parent` or lives under it. */
|
|
227
122
|
export function isInside(child, parent) {
|
|
228
123
|
const rel = relative(parent, child);
|
|
229
124
|
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
230
125
|
}
|
|
231
126
|
|
|
232
|
-
/**
|
|
233
|
-
* Read `.morph/config/config.json`, or null when it is absent or malformed.
|
|
234
|
-
*
|
|
235
|
-
* Exported so test-nodes.js reads the config through the SAME door: a second
|
|
236
|
-
* reader would be a second place for "absent" and "malformed" to drift apart —
|
|
237
|
-
* and both must keep meaning "fall back", never "fail".
|
|
238
|
-
*
|
|
239
|
-
* @param {string} projectPath
|
|
240
|
-
* @returns {object|null}
|
|
241
|
-
*/
|
|
127
|
+
/** The parsed config, or null when absent OR malformed — both mean "fall back", never "fail". */
|
|
242
128
|
export function readMorphConfig(projectPath) {
|
|
243
129
|
const configPath = join(projectPath, CONFIG_FILE_NAME);
|
|
244
130
|
if (!existsSync(configPath)) return null;
|
|
245
131
|
try {
|
|
246
132
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
247
133
|
} catch {
|
|
248
|
-
// Malformed config — treated as absent by every caller
|
|
249
134
|
return null;
|
|
250
135
|
}
|
|
251
136
|
}
|
|
252
137
|
|
|
253
|
-
/**
|
|
254
|
-
* Resolve the timeout budget for ONE test node.
|
|
255
|
-
*
|
|
256
|
-
* Precedence: explicit override (`morph-spec verify --timeout`) > the node's
|
|
257
|
-
* own `timeoutMs` > `config.project.testTimeoutMs` > default scaled by the
|
|
258
|
-
* number of test projects the node's command chains.
|
|
259
|
-
*
|
|
260
|
-
* The per-node ceiling sits above the project-wide one because the derived
|
|
261
|
-
* default (120s x chained projects) is shaped for `dotnet test`: it is hostile
|
|
262
|
-
* to a Node suite that takes twelve minutes, and a project with both runners
|
|
263
|
-
* cannot serve them with a single number. It sits BELOW the CLI override
|
|
264
|
-
* because what the user typed is the most explicit statement in the room.
|
|
265
|
-
*
|
|
266
|
-
* @param {string} projectPath
|
|
267
|
-
* @param {import('./test-nodes.js').TestNode} node - The node about to run
|
|
268
|
-
* @param {number} [override] - Explicit override in ms, if the caller has one
|
|
269
|
-
* @returns {number} Timeout in ms
|
|
270
|
-
*/
|
|
138
|
+
/** Precedence: CLI override > node `timeoutMs` > config `testTimeoutMs` > default × chained projects. */
|
|
271
139
|
export function resolveTestTimeout(projectPath, node, override) {
|
|
272
140
|
if (Number.isFinite(override) && override > 0) return override;
|
|
273
141
|
|
|
@@ -279,11 +147,7 @@ export function resolveTestTimeout(projectPath, node, override) {
|
|
|
279
147
|
return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, node?.projectCount ?? 1);
|
|
280
148
|
}
|
|
281
149
|
|
|
282
|
-
|
|
283
|
-
* Count the test runs that reported a summary before the command was cut off.
|
|
284
|
-
* Heuristic by construction (the summary line is localized), and used only to
|
|
285
|
-
* name WHICH link of the chain was still running when the clock ran out.
|
|
286
|
-
*/
|
|
150
|
+
// Heuristic: only names WHICH chain link was running when the clock ran out.
|
|
287
151
|
function countCompletedRuns(output) {
|
|
288
152
|
const matches = (output || '').match(
|
|
289
153
|
/(?:^|\n)\s*(?:Aprovado|Passed|Failed|Reprovado|Com falha|Erro)!/gi
|
|
@@ -291,27 +155,9 @@ function countCompletedRuns(output) {
|
|
|
291
155
|
return matches ? matches.length : 0;
|
|
292
156
|
}
|
|
293
157
|
|
|
294
|
-
// ============================================================================
|
|
295
|
-
// Prebuild of the test projects, and the `--no-build` refusal (T5 / UC-08)
|
|
296
|
-
// ============================================================================
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* `--no-build` as a standalone token. Anchored on whitespace so `--no-build-x`
|
|
300
|
-
* (or a path that happens to contain the word) never matches.
|
|
301
|
-
*/
|
|
302
158
|
const NO_BUILD_FLAG = /(^|\s)--no-build(\s|$)/;
|
|
303
159
|
|
|
304
|
-
|
|
305
|
-
* The `.csproj` targets a command names, quotes removed and duplicates dropped.
|
|
306
|
-
*
|
|
307
|
-
* Finding none yields `[]`, and `[]` is the SAFE answer, not a shrug: it means
|
|
308
|
-
* nothing was compiled here, which is precisely the condition that makes a
|
|
309
|
-
* `--no-build` in the same command a refusal. Refusing is cheap; approving a
|
|
310
|
-
* stale assembly is what F29 cost.
|
|
311
|
-
*
|
|
312
|
-
* @param {string|null|undefined} command
|
|
313
|
-
* @returns {string[]}
|
|
314
|
-
*/
|
|
160
|
+
// `[]` is the safe answer: nothing compiled here, so a `--no-build` in the same command is refused.
|
|
315
161
|
export function extractCsprojTargets(command) {
|
|
316
162
|
if (typeof command !== 'string') return [];
|
|
317
163
|
const found = [];
|
|
@@ -323,32 +169,79 @@ export function extractCsprojTargets(command) {
|
|
|
323
169
|
return [...new Set(found)];
|
|
324
170
|
}
|
|
325
171
|
|
|
326
|
-
/**
|
|
327
|
-
* Which test projects this node compiles before running: what it DECLARED, or
|
|
328
|
-
* else what its command names. Only `runner: 'dotnet'` has a prebuild at all —
|
|
329
|
-
* a vitest node has nothing to compile, and pretending otherwise would invent
|
|
330
|
-
* a build step nobody asked for.
|
|
331
|
-
*
|
|
332
|
-
* @param {import('./test-nodes.js').TestNode} node
|
|
333
|
-
* @returns {string[]}
|
|
334
|
-
*/
|
|
172
|
+
/** Declared `build`, else the csproj the command names; only `runner: 'dotnet'` prebuilds. */
|
|
335
173
|
export function resolveTestBuildTargets(node) {
|
|
336
174
|
if (node?.runner !== 'dotnet') return [];
|
|
337
175
|
if (Array.isArray(node.build) && node.build.length > 0) return node.build;
|
|
338
176
|
return extractCsprojTargets(node.command);
|
|
339
177
|
}
|
|
340
178
|
|
|
179
|
+
const DOTNET_BUILDABLE = /"([^"]+\.(?:slnx?|csproj))"|'([^']+\.(?:slnx?|csproj))'|(\S+\.(?:slnx?|csproj))/gi;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* What `dotnet test` compiles for this node, relative to its `cwd`: declared `build`,
|
|
183
|
+
* else what the command names, else the directory's single solution/project; `[]` = undiscoverable.
|
|
184
|
+
*/
|
|
185
|
+
export function discoverDotnetBuildTargets(node, cwd) {
|
|
186
|
+
if (node?.runner !== 'dotnet') return [];
|
|
187
|
+
if (Array.isArray(node.build) && node.build.length > 0) return node.build;
|
|
188
|
+
const command = typeof node.command === 'string' ? node.command : '';
|
|
189
|
+
const named = [...command.matchAll(DOTNET_BUILDABLE)].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
190
|
+
if (named.length > 0) return [...new Set(named)];
|
|
191
|
+
|
|
192
|
+
const dirArg = command.split(/\s+/).find((token) => token && !token.startsWith('-') && token !== 'dotnet' && token !== 'test'
|
|
193
|
+
&& isDirectory(join(cwd, token)));
|
|
194
|
+
const dir = dirArg ?? '.';
|
|
195
|
+
let entries;
|
|
196
|
+
try { entries = readdirSync(join(cwd, dir)); } catch { return []; }
|
|
197
|
+
const only = singleDotnetBuildable(entries);
|
|
198
|
+
return only ? [dir === '.' ? only : join(dir, only).replace(/\\/g, '/')] : [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function singleDotnetBuildable(files) {
|
|
202
|
+
const solutions = files.filter((f) => /\.slnx?$/i.test(f));
|
|
203
|
+
const projects = files.filter((f) => /\.csproj$/i.test(f));
|
|
204
|
+
if (solutions.length) return solutions.length === 1 ? solutions[0] : null;
|
|
205
|
+
return projects.length === 1 ? projects[0] : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const DOTNET_SCAN_SKIPPED = new Set([WORKTREES_DIR, LEGACY_WORKTREES_DIR, 'node_modules', 'bin', 'obj']);
|
|
209
|
+
const DOTNET_SCAN_DEPTH = 6;
|
|
210
|
+
|
|
211
|
+
function dotnetBuildablesUnder(root, rel = '', depth = 0, found = []) {
|
|
212
|
+
let entries;
|
|
213
|
+
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return found; }
|
|
214
|
+
for (const e of entries) {
|
|
215
|
+
const path = rel ? `${rel}/${e.name}` : e.name;
|
|
216
|
+
if (e.isFile() && /\.(?:slnx?|csproj)$/i.test(e.name)) found.push(path);
|
|
217
|
+
else if (e.isDirectory() && depth < DOTNET_SCAN_DEPTH && !e.name.startsWith('.') && !DOTNET_SCAN_SKIPPED.has(e.name)) {
|
|
218
|
+
dotnetBuildablesUnder(root, path, depth + 1, found);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
|
|
341
224
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* time anywhere in this repository.
|
|
345
|
-
*
|
|
346
|
-
* `--no-restore` is deliberately dropped: it is correct for the ROOT build
|
|
347
|
-
* (whose restore already happened) and wrong here, because the whole reason a
|
|
348
|
-
* test project needs its own prebuild is that it may live OUTSIDE the
|
|
349
|
-
* application's solution and may therefore never have been restored at all.
|
|
350
|
-
* F29's project was exactly that project.
|
|
225
|
+
* The repo's single .sln/.slnx, else its single .csproj. `target` is `.` when the root
|
|
226
|
+
* already has one, `null` for zero or several (listed in `candidates`).
|
|
351
227
|
*/
|
|
228
|
+
export function discoverDotnetSolution(root) {
|
|
229
|
+
let rootEntries;
|
|
230
|
+
try { rootEntries = readdirSync(root); } catch { return { target: null, candidates: [] }; }
|
|
231
|
+
if (rootEntries.some((e) => /\.(?:slnx?|csproj)$/i.test(e))) return { target: '.', candidates: [] };
|
|
232
|
+
const all = dotnetBuildablesUnder(root).sort();
|
|
233
|
+
const solutions = all.filter((f) => /\.slnx?$/i.test(f));
|
|
234
|
+
const pool = solutions.length ? solutions : all;
|
|
235
|
+
const single = singleDotnetBuildable(pool);
|
|
236
|
+
return { target: single, candidates: pool };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isDirectory(path) {
|
|
240
|
+
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Derived from build-check.js, the single spelling of "dotnet build". `--no-restore` is
|
|
244
|
+
// dropped: a test project outside the solution may never have been restored.
|
|
352
245
|
export function dotnetBuildCommandFor(target, projectPath) {
|
|
353
246
|
const base = getBuildSteps('dotnet', { cwd: projectPath || '.' })[0]?.command;
|
|
354
247
|
if (!base) return null;
|
|
@@ -357,21 +250,8 @@ export function dotnetBuildCommandFor(target, projectPath) {
|
|
|
357
250
|
return `${verb} ${subcommand} "${target}"${kept.length ? ` ${kept.join(' ')}` : ''}`;
|
|
358
251
|
}
|
|
359
252
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
* happened in this invocation — the fact the `--no-build` refusal keys on.
|
|
363
|
-
*
|
|
364
|
-
* WHEN it runs: when the node declared `build[]` (an explicit statement that
|
|
365
|
-
* these projects need compiling), or when the command carries `--no-build`
|
|
366
|
-
* (the only case where the binary would otherwise be whatever the last run
|
|
367
|
-
* left behind). A plain `dotnet test X.csproj` is NOT pre-compiled: `dotnet
|
|
368
|
-
* test` builds it itself and a compile error already reddens the node, so
|
|
369
|
-
* pre-compiling would only double the clock of the hot path — and the clock is
|
|
370
|
-
* what D13 is about.
|
|
371
|
-
*
|
|
372
|
-
* @returns {{ built: boolean, failure: object|null }} `failure` is a full
|
|
373
|
-
* runTestSuite result, ready to be returned as-is.
|
|
374
|
-
*/
|
|
253
|
+
// Runs only with declared `build[]` or `--no-build`: a plain `dotnet test` builds
|
|
254
|
+
// itself, and pre-compiling it would double the clock of the hot path.
|
|
375
255
|
function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
|
|
376
256
|
if (node.runner !== 'dotnet') return { built: false, failure: null };
|
|
377
257
|
|
|
@@ -407,25 +287,13 @@ function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
|
|
|
407
287
|
return { built: targets.length > 0, failure: null };
|
|
408
288
|
}
|
|
409
289
|
|
|
410
|
-
// ============================================================================
|
|
411
|
-
// How many tests actually ran (T4 / UC-08)
|
|
412
|
-
// ============================================================================
|
|
413
|
-
|
|
414
|
-
/** VSTest: one summary line per project, STARTING with the verdict. */
|
|
415
290
|
const VSTEST_SUMMARY = /^\s*(?:Aprovado|Passed|Com falha|Failed|Reprovado|Erro)!/i;
|
|
416
|
-
|
|
417
|
-
* Microsoft.Testing.Platform: the header line, in the two locales MEASURED (pt-BR and
|
|
418
|
-
* en-US). `\S+` spans `ção` — and a mis-decoded version of it — without naming a byte.
|
|
419
|
-
*/
|
|
291
|
+
// `\S+` spans `ção`, and a mis-decoded version of it, without naming a byte.
|
|
420
292
|
const MTP_SUMMARY_HEADER = /^(?:Test run summary|Resumo da execu\S+ de teste):/;
|
|
421
293
|
const MTP_TOTAL = /^\s+total:\s*(\d+)\s*$/;
|
|
422
294
|
const MTP_FAILED = /^\s+(?:failed|falhou):\s*(\d+)\s*$/;
|
|
423
295
|
const MTP_PASSED = /^\s+(?:succeeded|conclu\S*\s+com\s+\S*xito):\s*(\d+)\s*$/;
|
|
424
296
|
|
|
425
|
-
/**
|
|
426
|
-
* The indented block under an MTP summary header. It ends at the first line that
|
|
427
|
-
* starts at column 0; a block with no `total:` line yields `total: null`.
|
|
428
|
-
*/
|
|
429
297
|
function mtpSummaryBlock(lines, from) {
|
|
430
298
|
const block = { total: null, passed: 0, failed: 0 };
|
|
431
299
|
for (let i = from; i < lines.length && /^(?:\s|$)/.test(lines[i]); i++) {
|
|
@@ -438,28 +306,9 @@ function mtpSummaryBlock(lines, from) {
|
|
|
438
306
|
return block;
|
|
439
307
|
}
|
|
440
308
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
* - **VSTest**: a line per project that starts with the verdict —
|
|
445
|
-
* `Aprovado! - Com falha: 0, Aprovado: 57, Ignorado: 1, Total: 58, …`.
|
|
446
|
-
* - **Microsoft.Testing.Platform** (xunit.v3 4.x under `global.json`
|
|
447
|
-
* `test.runner`, #115): the verdict does NOT start the line —
|
|
448
|
-
* `Test run summary: Passed!` / `Resumo da execução de teste: Aprovado!` —
|
|
449
|
-
* and the count comes in an indented block below it, with a lowercase
|
|
450
|
-
* `total:`, `failed:`/`falhou:` and `succeeded:`/`concluído com êxito:`.
|
|
451
|
-
* Measured in both locales (test/fixtures/test-output/mtp-*.txt). ONE block
|
|
452
|
-
* per `dotnet test` invocation: a solution of two projects prints a single
|
|
453
|
-
* aggregated block (mtp-solution-fail-skip.txt: `total: 6` = 4 + 2), so
|
|
454
|
-
* blocks add up only across invocations. `total` counts skipped cases, as
|
|
455
|
-
* VSTest's `Total:` does. Another locale is not read: UNKNOWN, never 0.
|
|
456
|
-
*
|
|
457
|
-
* SUM, not "the last one": a chained `a && b` command prints one summary per
|
|
458
|
-
* invocation, and reading a single one would report half a suite as the whole
|
|
459
|
-
* of it — which, against a recorded baseline, is an invented count drop.
|
|
460
|
-
* `countCompletedRuns()` above counts the VSTest lines for a different question
|
|
461
|
-
* (WHICH link was running when the clock died); this one reads their numbers.
|
|
462
|
-
*/
|
|
309
|
+
// SUM of every VSTest line and MTP block: a chained `a && b` prints one summary per
|
|
310
|
+
// invocation, and reading only one invents a count drop. MTP prints ONE aggregated
|
|
311
|
+
// block per invocation; an unmeasured locale is UNKNOWN, never 0.
|
|
463
312
|
function parseDotnetCount(output) {
|
|
464
313
|
const lines = output.split('\n');
|
|
465
314
|
let seen = false;
|
|
@@ -488,30 +337,6 @@ function parseDotnetCount(output) {
|
|
|
488
337
|
return seen ? sum : null;
|
|
489
338
|
}
|
|
490
339
|
|
|
491
|
-
/**
|
|
492
|
-
* The `total` of every MTP summary block in `output`, ANSI stripped — one per
|
|
493
|
-
* `dotnet test` invocation (measured: a solution prints ONE aggregated block).
|
|
494
|
-
* `[]` when none is readable. Read by `judgeNarrowedRun` (verify-runner.js): in a
|
|
495
|
-
* narrowed `&&` chain the SUM can be > 0 while one link ran nothing — its exit 8
|
|
496
|
-
* ignored by a path the scanner cannot read — and that link's own block says
|
|
497
|
-
* `total: 0`.
|
|
498
|
-
*
|
|
499
|
-
* @param {string|null|undefined} output
|
|
500
|
-
* @returns {number[]}
|
|
501
|
-
*/
|
|
502
|
-
export function mtpSummaryTotals(output) {
|
|
503
|
-
if (typeof output !== 'string' || !output) return [];
|
|
504
|
-
const lines = stripAnsi(output).split('\n');
|
|
505
|
-
const totals = [];
|
|
506
|
-
lines.forEach((line, i) => {
|
|
507
|
-
if (!MTP_SUMMARY_HEADER.test(line)) return;
|
|
508
|
-
const { total } = mtpSummaryBlock(lines, i + 1);
|
|
509
|
-
if (total !== null) totals.push(total);
|
|
510
|
-
});
|
|
511
|
-
return totals;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
|
|
515
340
|
function lastSummaryValue(output, key) {
|
|
516
341
|
const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
|
|
517
342
|
let match;
|
|
@@ -520,7 +345,6 @@ function lastSummaryValue(output, key) {
|
|
|
520
345
|
return value;
|
|
521
346
|
}
|
|
522
347
|
|
|
523
|
-
/** `node --test`, in either of the two reporters the runner may pick. */
|
|
524
348
|
function parseNodeTestCount(output) {
|
|
525
349
|
const total = lastSummaryValue(output, 'tests');
|
|
526
350
|
if (total === null) return null;
|
|
@@ -531,13 +355,7 @@ function parseNodeTestCount(output) {
|
|
|
531
355
|
};
|
|
532
356
|
}
|
|
533
357
|
|
|
534
|
-
|
|
535
|
-
* Vitest: `Tests 3082 passed (3082)` / `Tests 1 failed | 3081 passed (3082)`.
|
|
536
|
-
*
|
|
537
|
-
* The line must start with `Tests`, never `Test Files` — the file count sitting
|
|
538
|
-
* right above it has the same shape, and reading it would report 27 where 3082
|
|
539
|
-
* ran. A fabricated drop is worse than no count at all.
|
|
540
|
-
*/
|
|
358
|
+
// `Tests`, never `Test Files`: the file-count line above has the same shape.
|
|
541
359
|
function parseVitestCount(output) {
|
|
542
360
|
const line = output.split('\n').find((l) => /^\s*Tests\s{1,}\d/.test(l));
|
|
543
361
|
if (!line) return null;
|
|
@@ -550,7 +368,6 @@ function parseVitestCount(output) {
|
|
|
550
368
|
return { total: declaredTotal ? Number(declaredTotal[1]) : p + f, passed: p, failed: f };
|
|
551
369
|
}
|
|
552
370
|
|
|
553
|
-
/** Jest: `Tests: 2 failed, 118 passed, 120 total`. */
|
|
554
371
|
function parseJestCount(output) {
|
|
555
372
|
const line = output.split('\n').find((l) => /^\s*Tests:\s/.test(l));
|
|
556
373
|
if (!line) return null;
|
|
@@ -565,33 +382,14 @@ function parseJestCount(output) {
|
|
|
565
382
|
};
|
|
566
383
|
}
|
|
567
384
|
|
|
568
|
-
/**
|
|
569
|
-
* CSI (`ESC [ ... <final byte @-~>`, e.g. SGR color codes) and OSC (`ESC ] ...
|
|
570
|
-
* BEL|ESC\`, e.g. hyperlinks/terminal titles) escape sequences — the two
|
|
571
|
-
* shapes a captured terminal transcript actually carries (issue #110: a
|
|
572
|
-
* vitest summary piped through `execSync` kept its SGR bytes, and every count
|
|
573
|
-
* regex in this file is anchored on `^` / exact literals, so a color code
|
|
574
|
-
* sitting between "Tests" and the number silently defeated the whole line).
|
|
575
|
-
*/
|
|
576
385
|
const ANSI_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
|
|
577
386
|
|
|
578
|
-
/**
|
|
579
|
-
* Strip ANSI escape sequences (CSI/SGR and OSC) from captured process output.
|
|
580
|
-
*
|
|
581
|
-
* Non-string input is returned UNCHANGED, never coerced to `''` — the callers
|
|
582
|
-
* that matter (`parseTestCount`, `isZeroTestRun`) already treat a non-string
|
|
583
|
-
* output as "nothing to read" on their own; making this function opinionated
|
|
584
|
-
* about that too would be a second place for the same rule to drift.
|
|
585
|
-
*
|
|
586
|
-
* @param {string} text
|
|
587
|
-
* @returns {string} `text` with escape sequences removed; unchanged if not a string
|
|
588
|
-
*/
|
|
387
|
+
/** Strips CSI/SGR and OSC sequences; non-string input is returned unchanged, never coerced. */
|
|
589
388
|
export function stripAnsi(text) {
|
|
590
389
|
if (typeof text !== 'string') return text;
|
|
591
390
|
return text.replace(ANSI_PATTERN, '');
|
|
592
391
|
}
|
|
593
392
|
|
|
594
|
-
/** Playwright: `8 passed (12.4s)` and/or `2 failed`, on their own lines. */
|
|
595
393
|
function parsePlaywrightCount(output) {
|
|
596
394
|
const passed = /^\s*(\d+)\s+passed\b/im.exec(output);
|
|
597
395
|
const failed = /^\s*(\d+)\s+failed\b/im.exec(output);
|
|
@@ -601,15 +399,10 @@ function parsePlaywrightCount(output) {
|
|
|
601
399
|
return { total: p + f, passed: p, failed: f };
|
|
602
400
|
}
|
|
603
401
|
|
|
604
|
-
/** One extractor per runner. An absent entry is an honest "I cannot read this". */
|
|
605
402
|
const COUNT_EXTRACTORS = {
|
|
606
403
|
dotnet: parseDotnetCount,
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
// by is the `scenarios` field of the fixture's summary FILE, which this
|
|
610
|
-
// function cannot see: `judgeEvalRun` replaces this count with it, or with
|
|
611
|
-
// `null` (UNKNOWN, never zero) when the file is unreadable, and `judgeRun`
|
|
612
|
-
// never compares an eval node against `expectedCount` (#110).
|
|
404
|
+
// Counts xunit cases, not scenarios: `judgeEvalRun` replaces it with the summary
|
|
405
|
+
// file's scenario count (or null), and `judgeRun` never compares it to `expectedCount`.
|
|
613
406
|
evals: parseDotnetCount,
|
|
614
407
|
'node-test': parseNodeTestCount,
|
|
615
408
|
vitest: parseVitestCount,
|
|
@@ -619,135 +412,25 @@ const COUNT_EXTRACTORS = {
|
|
|
619
412
|
};
|
|
620
413
|
|
|
621
414
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
* @param {string|null|undefined} output - Combined stdout+stderr of the run
|
|
625
|
-
* @param {string} runner - One of TEST_RUNNERS (see test-nodes.js)
|
|
626
|
-
* @returns {{ total: number, passed: number, failed: number }|null}
|
|
627
|
-
* **null is UNKNOWN, never zero.** A summary nobody could read is not a
|
|
628
|
-
* suite that ran zero tests: collapsing the two would manufacture a total
|
|
629
|
-
* count drop against every recorded baseline out of a missing regex. Same
|
|
630
|
-
* doctrine as `null` in pr-index.js.
|
|
415
|
+
* Returns null when the summary is unreadable: null is UNKNOWN, never zero, or a
|
|
416
|
+
* missing regex would fabricate a count drop against every baseline.
|
|
631
417
|
*/
|
|
632
418
|
export function parseTestCount(output, runner) {
|
|
633
419
|
if (typeof output !== 'string' || !output) return null;
|
|
634
420
|
const extractor = COUNT_EXTRACTORS[runner];
|
|
635
421
|
if (!extractor) return null;
|
|
636
|
-
//
|
|
637
|
-
// bytes — a captured terminal transcript may carry SGR color codes right
|
|
638
|
-
// inside a summary line (#110) and every regex above is anchored on exact
|
|
639
|
-
// literals that a stray `\x1b[32m` would defeat.
|
|
422
|
+
// Strip ANSI before EVERY extractor: an SGR code inside a summary line defeats the anchored regexes.
|
|
640
423
|
return extractor(stripAnsi(output));
|
|
641
424
|
}
|
|
642
425
|
|
|
643
|
-
// ============================================================================
|
|
644
|
-
// "Zero tests ran" signatures, per runner (#87 PR B / #110 parte 1)
|
|
645
|
-
// ============================================================================
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Per-runner signal for "the run matched/executed NOTHING" — a category
|
|
649
|
-
* distinct from both a genuine `{ total: 0, ... }` summary (a suite that
|
|
650
|
-
* legitimately declares zero cases) and from a summary `parseTestCount`
|
|
651
|
-
* simply could not read (`null`, UNKNOWN). Its reader is `judgeNarrowedRun`
|
|
652
|
-
* (verify-runner.js, #87): a narrowed `verify {feature} {task}` run that FAILED
|
|
653
|
-
* with this signature, and with no count proving tests ran, is named
|
|
654
|
-
* `narrowed-zero-tests (…)` instead of an anonymous red. (A PASSED narrowed run
|
|
655
|
-
* needs its own proof there — a count > 0, or MTP's exit 0 — never this table.)
|
|
656
|
-
*
|
|
657
|
-
* Every entry was MEASURED against a real runner, not invented, and the two
|
|
658
|
-
* dotnet entries disagree with each other on purpose:
|
|
659
|
-
*
|
|
660
|
-
* - **MTP** (`--filter-class`, this repo's own `ai-kit`, xunit.v3 4.x):
|
|
661
|
-
* a non-matching filter exits **8** (`ZeroTests`) — reliable BY EXIT
|
|
662
|
-
* CODE. See `test/fixtures/test-output/mtp-zero-tests.txt`.
|
|
663
|
-
* - **VSTest** (`--filter "FullyQualifiedName~..."`, classic xunit 2.x):
|
|
664
|
-
* a non-matching filter exits **0** — exit code CANNOT tell a zero-match
|
|
665
|
-
* run from a green one here. Only the OUTPUT STRING can:
|
|
666
|
-
* `test/fixtures/test-output/vstest-zero-tests.txt` (pt-BR, measured).
|
|
667
|
-
* The en-US wording is the brief's documented string, not measured on
|
|
668
|
-
* this pt-BR machine — kept alongside the pt-BR one because neither is
|
|
669
|
-
* derivable from the other and the machine that runs `verify` may be in
|
|
670
|
-
* either locale.
|
|
671
|
-
*
|
|
672
|
-
* Exit code 8 is therefore listed ONLY as a dotnet exit code, never paired
|
|
673
|
-
* with a string requirement — and exit code 9
|
|
674
|
-
* (`MinimumExpectedTestsPolicyViolation`, this repo's own
|
|
675
|
-
* `--minimum-expected-tests 275`) is deliberately ABSENT: it fires on a run
|
|
676
|
-
* that executed fewer cases than the configured floor, including a run that
|
|
677
|
-
* was entirely green (measured:
|
|
678
|
-
* `test/fixtures/test-output/mtp-filter-class-policy-violation.txt`, 10/10
|
|
679
|
-
* passed, exit 9). Folding 9 into this table would misreport a real pass as
|
|
680
|
-
* "zero tests ran".
|
|
681
|
-
*/
|
|
682
|
-
export const ZERO_RUN_SIGNATURES = {
|
|
683
|
-
dotnet: {
|
|
684
|
-
exitCodes: [8],
|
|
685
|
-
strings: [
|
|
686
|
-
// en-US, VSTest — documented by the brief, not measured here (pt-BR machine).
|
|
687
|
-
'No test matches the given testcase filter',
|
|
688
|
-
// pt-BR, VSTest — MEASURED, test/fixtures/test-output/vstest-zero-tests.txt.
|
|
689
|
-
'Nenhum teste corresponde ao filtro de testcase',
|
|
690
|
-
],
|
|
691
|
-
},
|
|
692
|
-
vitest: {
|
|
693
|
-
exitCodes: [],
|
|
694
|
-
strings: ['No test files found'],
|
|
695
|
-
},
|
|
696
|
-
jest: {
|
|
697
|
-
exitCodes: [],
|
|
698
|
-
strings: ['No tests found'],
|
|
699
|
-
},
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* True when `output`/`exitCode` carries this runner's "zero tests ran"
|
|
704
|
-
* signature. Every other runner (`node-test`, `playwright`, `evals`, `none`,
|
|
705
|
-
* or anything not in `ZERO_RUN_SIGNATURES`) answers `false` — an absent entry
|
|
706
|
-
* is an honest "this primitive does not know that runner's zero-tests shape
|
|
707
|
-
* yet", the same doctrine `COUNT_EXTRACTORS` uses for an unknown runner.
|
|
708
|
-
*
|
|
709
|
-
* Called by `judgeNarrowedRun` (verify-runner.js) on a FAILED narrowed run, to
|
|
710
|
-
* name it `narrowed-zero-tests (…)`.
|
|
711
|
-
*
|
|
712
|
-
* @param {string|null|undefined} output - Combined stdout+stderr of the run
|
|
713
|
-
* @param {string} runner - One of TEST_RUNNERS
|
|
714
|
-
* @param {number|null|undefined} exitCode - Must be a NUMBER to match: the
|
|
715
|
-
* exit-code check is a strict `includes`, so the string `'8'` never matches 8.
|
|
716
|
-
* @returns {boolean}
|
|
717
|
-
*/
|
|
718
|
-
export function isZeroTestRun(output, runner, exitCode) {
|
|
719
|
-
const signature = ZERO_RUN_SIGNATURES[runner];
|
|
720
|
-
if (!signature) return false;
|
|
721
|
-
|
|
722
|
-
if (signature.exitCodes.includes(exitCode)) return true;
|
|
723
|
-
|
|
724
|
-
const text = stripAnsi(typeof output === 'string' ? output : '');
|
|
725
|
-
return signature.strings.some((needle) => text.includes(needle));
|
|
726
|
-
}
|
|
727
|
-
|
|
728
426
|
/**
|
|
729
|
-
*
|
|
730
|
-
* and
|
|
731
|
-
*
|
|
732
|
-
* A count that DROPS is a finding: tests that used to run no longer do, which
|
|
733
|
-
* is exactly the silent damage this feature exists to catch. A count that
|
|
734
|
-
* RISES is the normal life of a suite that grows with every task. A symmetric
|
|
735
|
-
* rule would turn each new test into a red gate and would teach the agent to
|
|
736
|
-
* delete tests in order to "fix" it — the lesson MORPH.md already paid for
|
|
737
|
-
* (a typed number goes stale on the first task).
|
|
738
|
-
*
|
|
739
|
-
* @param {number|null} count - Executed count; null is UNKNOWN
|
|
740
|
-
* @param {number|null} expectedCount - Recorded baseline; null is "none recorded"
|
|
741
|
-
* @returns {{ verdict: 'drop'|'rise'|'match'|'unknown'|'no-baseline', reason: string|null, warning: string|null }}
|
|
742
|
-
* `reason` is non-null ONLY for a drop — it is the only verdict that fails a
|
|
743
|
-
* node by itself. Everything else leaves the verdict to the exit code.
|
|
427
|
+
* Asymmetric on purpose: only a drop fails (non-null `reason`); a rise only warns,
|
|
428
|
+
* or every new test would redden the gate and teach deleting tests.
|
|
744
429
|
*/
|
|
745
430
|
export function compareTestCount(count, expectedCount) {
|
|
746
431
|
const answer = (verdict, reason = null, warning = null) => ({ verdict, reason, warning });
|
|
747
432
|
|
|
748
|
-
//
|
|
749
|
-
// distinction has to survive here or "this suite runs zero tests" becomes
|
|
750
|
-
// inexpressible.
|
|
433
|
+
// A declared `0` is a real baseline; only null means "none recorded".
|
|
751
434
|
if (!Number.isInteger(expectedCount)) return answer('no-baseline');
|
|
752
435
|
|
|
753
436
|
if (!Number.isInteger(count)) {
|
|
@@ -768,25 +451,8 @@ export function compareTestCount(count, expectedCount) {
|
|
|
768
451
|
}
|
|
769
452
|
|
|
770
453
|
/**
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* The node is a parameter rather than something resolved here: a project has a
|
|
774
|
-
* LIST of suites (see test-nodes.js), and a runner that resolved its own single
|
|
775
|
-
* command is precisely what let 3.082 vitest tests sit outside the gate (F30).
|
|
776
|
-
* Resolution belongs to `resolveTestNodes()`; execution belongs here. One node,
|
|
777
|
-
* one command, one cwd, one clock.
|
|
778
|
-
*
|
|
779
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
780
|
-
* @param {object} [opts]
|
|
781
|
-
* @param {import('./test-nodes.js').TestNode} opts.node - The node to run; its
|
|
782
|
-
* `cwd` is resolved against `projectPath`
|
|
783
|
-
* @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
|
|
784
|
-
* @param {Record<string,string>} [opts.env] - Extra environment for THIS node, layered
|
|
785
|
-
* over `process.env`. The eval runner's channel (`MORPH_EVAL_*`) travels here.
|
|
786
|
-
* Absent leaves the child on the parent's environment, byte for byte — every
|
|
787
|
-
* other runner keeps the exact behaviour it had.
|
|
788
|
-
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
789
|
-
* @returns {{ passed?: boolean, output?: string, exitCode?: number, skipped?: boolean, reason?: string, timedOut?: boolean, timeoutMs?: number, message?: string }}
|
|
454
|
+
* Runs ONE node; resolving the list of nodes belongs to `resolveTestNodes()`.
|
|
455
|
+
* `opts.env` is layered over `process.env` (the eval channel `MORPH_EVAL_*` travels there).
|
|
790
456
|
*/
|
|
791
457
|
export function runTestSuite(projectPath = '.', opts = {}) {
|
|
792
458
|
const node = opts.node;
|
|
@@ -804,17 +470,12 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
804
470
|
const projectCount = node.projectCount ?? 1;
|
|
805
471
|
const cwd = resolve(projectPath, node.cwd || '.');
|
|
806
472
|
|
|
807
|
-
//
|
|
808
|
-
// anything behind it. Order matters: the build is what makes the flag legal.
|
|
473
|
+
// Prebuild BEFORE judging `--no-build`: the build is what makes the flag legal.
|
|
809
474
|
const prebuild = runTestPrebuild({ node, cwd, exec, timeout, projectPath });
|
|
810
475
|
if (prebuild.failure) return prebuild.failure;
|
|
811
476
|
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// executed. The refusal keys on whether a build HAPPENED here, never on the
|
|
815
|
-
// flag alone — `--no-build` after a real build is the fast path (D11). The
|
|
816
|
-
// command itself is never rewritten: a silent fix would hide the config
|
|
817
|
-
// defect and make the reported `command` a lie about what ran.
|
|
477
|
+
// `--no-build` without a build here would run a stale assembly. The command is never
|
|
478
|
+
// rewritten: a silent fix would make the reported `command` a lie about what ran.
|
|
818
479
|
if (node.runner === 'dotnet' && NO_BUILD_FLAG.test(cmd) && !prebuild.built) {
|
|
819
480
|
return {
|
|
820
481
|
passed: false,
|
|
@@ -826,8 +487,7 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
826
487
|
};
|
|
827
488
|
}
|
|
828
489
|
|
|
829
|
-
// Layered OVER process.env
|
|
830
|
-
// fail in a way that has nothing to do with the suite under test.
|
|
490
|
+
// Layered OVER process.env: a child that lost PATH fails for reasons unrelated to the suite.
|
|
831
491
|
const env = opts.env ? { ...process.env, ...opts.env } : undefined;
|
|
832
492
|
|
|
833
493
|
try {
|
|
@@ -840,11 +500,11 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
840
500
|
});
|
|
841
501
|
return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
|
|
842
502
|
} catch (err) {
|
|
843
|
-
|
|
503
|
+
// A spawn failure or a kill leaves both streams empty; its own error is then the only lead.
|
|
504
|
+
const output = (err.stdout || '') + (err.stderr || '')
|
|
505
|
+
|| [err.code, err.signal, err.message].filter(Boolean).join(' ');
|
|
844
506
|
|
|
845
|
-
// A timeout is NOT a red suite: the command was interrupted, so no verdict
|
|
846
|
-
// exists. Saying so by name is the whole point — the field diagnosis of
|
|
847
|
-
// this cost a reading of durationMs against a truncated log.
|
|
507
|
+
// A timeout is NOT a red suite: the command was interrupted, so no verdict exists.
|
|
848
508
|
if (err.code === 'ETIMEDOUT' || err.killed === true) {
|
|
849
509
|
const total = Math.max(1, projectCount);
|
|
850
510
|
const at = Math.min(countCompletedRuns(output) + 1, total);
|