@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- 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 +75 -18
- 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-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- 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 +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -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/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- 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 +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- 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/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- 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/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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Verify Runner — deterministic build+tests+validators+e2e cadence.
|
|
3
3
|
*
|
|
4
4
|
* v8 adds a DETERMINISTIC verification node that runs BEFORE the LLM judge
|
|
5
|
-
* in the
|
|
5
|
+
* in the implement loop: build + tests + validators (code, free) first; the
|
|
6
6
|
* LLM judge is only invoked once verify passes. This is a BINARY gate
|
|
7
7
|
* (pass/fail per node).
|
|
8
8
|
*
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* - runBuildCheck() from framework/hooks/shared/build-check.js
|
|
16
16
|
* - runTestSuite() from src/lib/tasks/test-runner.js (one node)
|
|
17
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
18
|
* - runValidation() from ./validation-runner.js
|
|
20
19
|
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
21
20
|
*
|
|
@@ -26,16 +25,15 @@ import { execFileSync } from 'child_process';
|
|
|
26
25
|
import { existsSync, readFileSync, rmSync } from 'fs';
|
|
27
26
|
import { join, resolve } from 'path';
|
|
28
27
|
|
|
29
|
-
import { runBuildCheck, getBuildSteps } from '../../../framework/hooks/shared/build-check.js';
|
|
30
|
-
import { runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
28
|
+
import { runBuildCheck, getBuildSteps, isDotnetStack } from '../../../framework/hooks/shared/build-check.js';
|
|
29
|
+
import { composeProjectName, runE2eCheck } from '../../../framework/hooks/shared/e2e-check.js';
|
|
31
30
|
import { buildComposeEnv } from '../e2e/compose-env.js';
|
|
32
31
|
import {
|
|
33
|
-
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout,
|
|
32
|
+
runTestSuite, parseTestCount, compareTestCount, resolveTestTimeout, discoverDotnetSolution,
|
|
34
33
|
} from '../tasks/test-runner.js';
|
|
35
34
|
import {
|
|
36
35
|
resolveTestNodes, parseTestFilter, nodeMatchesFilter, recordTestBaselines, morphConfigPath,
|
|
37
36
|
} from '../tasks/test-nodes.js';
|
|
38
|
-
import { applyTaskScope } from '../tasks/test-scope.js';
|
|
39
37
|
import { runLinuxSuite, checkLinuxPreconditions } from '../tasks/linux-runner.js';
|
|
40
38
|
import {
|
|
41
39
|
EVAL_RUNNER, evalPreflight, evalNodeEnv, readEvalSummary, evalScenarioCount,
|
|
@@ -44,7 +42,6 @@ import {
|
|
|
44
42
|
} from '../tasks/eval-node.js';
|
|
45
43
|
import { runValidation } from './validation-runner.js';
|
|
46
44
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
47
|
-
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
48
45
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
49
46
|
|
|
50
47
|
/** Default build timeout — dotnet cold build is slow; 30s (the Stop-hook default) is too short. */
|
|
@@ -56,14 +53,6 @@ const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
|
56
53
|
/** Number of trailing output lines kept for feedback/reporting. */
|
|
57
54
|
const OUTPUT_TAIL_LINES = 20;
|
|
58
55
|
|
|
59
|
-
/** A narrowed run that proved ZERO tests ran (#87) — wording pinned by the plan. */
|
|
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
56
|
/**
|
|
68
57
|
* The caller asked for something impossible — not a red verification, a wrong
|
|
69
58
|
* invocation. The distinction is the exit code: `verify` answers 1 for "the
|
|
@@ -82,11 +71,10 @@ export class VerifyUsageError extends Error {
|
|
|
82
71
|
}
|
|
83
72
|
|
|
84
73
|
/**
|
|
85
|
-
* Run the deterministic verify cadence for a feature
|
|
74
|
+
* Run the deterministic verify cadence for a feature.
|
|
86
75
|
*
|
|
87
76
|
* @param {string} projectPath - Project root directory
|
|
88
77
|
* @param {string} featureName - Feature name
|
|
89
|
-
* @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
|
|
90
78
|
* @param {Object} [options]
|
|
91
79
|
* @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
|
|
92
80
|
* (default: 120000 for build; for tests, see resolveTestTimeout())
|
|
@@ -99,8 +87,6 @@ export class VerifyUsageError extends Error {
|
|
|
99
87
|
* @param {boolean} [options.linux] - Run the test nodes inside a Linux container, on a COPY
|
|
100
88
|
* of the tree (see linux-runner.js for why a bind mount would prove nothing)
|
|
101
89
|
* @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
90
|
* @param {Function} [options._runBuildCheck] - Injectable override for runBuildCheck (testing)
|
|
105
91
|
* @param {Function} [options._runTestSuite] - Injectable override for runTestSuite (testing)
|
|
106
92
|
* @param {Function} [options._runValidation] - Injectable override for runValidation (testing)
|
|
@@ -110,16 +96,16 @@ export class VerifyUsageError extends Error {
|
|
|
110
96
|
* @returns {Promise<Object>} Aggregated verify result — see module docs for shape
|
|
111
97
|
* @throws {HeavyBusyError} when no heavy slot (or RAM) freed within `heavy.waitTimeoutSec`
|
|
112
98
|
*/
|
|
113
|
-
export async function runVerify(projectPath, featureName,
|
|
99
|
+
export async function runVerify(projectPath, featureName, options = {}) {
|
|
114
100
|
const gate = options._heavyGate ?? createHeavyGate({ projectPath, feature: featureName });
|
|
115
101
|
try {
|
|
116
|
-
return await runVerifyGated(projectPath, featureName,
|
|
102
|
+
return await runVerifyGated(projectPath, featureName, options, gate);
|
|
117
103
|
} finally {
|
|
118
104
|
gate.release();
|
|
119
105
|
}
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
async function runVerifyGated(projectPath, featureName,
|
|
108
|
+
async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
123
109
|
const {
|
|
124
110
|
timeout = DEFAULT_BUILD_TIMEOUT,
|
|
125
111
|
skipTests = false,
|
|
@@ -130,7 +116,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
130
116
|
refreshCache = false,
|
|
131
117
|
linux = false,
|
|
132
118
|
linuxImage = null,
|
|
133
|
-
full = false,
|
|
134
119
|
_runLinuxSuite = runLinuxSuite,
|
|
135
120
|
_exec = undefined,
|
|
136
121
|
_runBuildCheck = runBuildCheck,
|
|
@@ -156,18 +141,13 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
156
141
|
);
|
|
157
142
|
}
|
|
158
143
|
|
|
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
144
|
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
165
145
|
// discovering that there is nowhere to write AFTER running the whole fleet
|
|
166
146
|
// would charge the user a full suite for a usage error. `config.json` is
|
|
167
147
|
// never created here — this repository deliberately has none, and inventing
|
|
168
148
|
// one would break `doctor --full`.
|
|
169
149
|
if (recordBaseline) {
|
|
170
|
-
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests
|
|
150
|
+
const problem = baselineTargetProblem(projectPath, testPlan, { skipTests });
|
|
171
151
|
if (problem) throw new VerifyUsageError(problem);
|
|
172
152
|
}
|
|
173
153
|
|
|
@@ -206,16 +186,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
206
186
|
}
|
|
207
187
|
}
|
|
208
188
|
|
|
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
189
|
const nodes = {};
|
|
220
190
|
const feedback = [];
|
|
221
191
|
|
|
@@ -229,27 +199,24 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
229
199
|
// default down would silently outrank config.project.testTimeoutMs and the
|
|
230
200
|
// per-project scaling (see resolveTestTimeout()).
|
|
231
201
|
const explicitTimeout = Number.isFinite(options.timeout) ? options.timeout : undefined;
|
|
202
|
+
const stackNodes = [];
|
|
232
203
|
nodes.tests = await runTestsNode({
|
|
233
204
|
projectPath,
|
|
234
|
-
testPlan
|
|
205
|
+
testPlan,
|
|
235
206
|
filter,
|
|
236
207
|
skipTests,
|
|
237
208
|
buildNode: nodes.build,
|
|
238
209
|
timeout: explicitTimeout,
|
|
239
|
-
taskId,
|
|
240
210
|
refreshCache,
|
|
241
211
|
linux,
|
|
242
212
|
linuxImage,
|
|
243
213
|
gate,
|
|
214
|
+
stackNodes,
|
|
244
215
|
_runTestSuite,
|
|
245
216
|
_runLinuxSuite,
|
|
246
217
|
_exec,
|
|
247
218
|
});
|
|
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);
|
|
219
|
+
if (stackNodes.length === 0) appendTestsFeedback(feedback, nodes.tests);
|
|
253
220
|
|
|
254
221
|
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
255
222
|
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
@@ -258,28 +225,30 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
258
225
|
// 4. e2e — the only node that runs the application. Like tests, it is not
|
|
259
226
|
// invoked at all on a red build: a stack that cannot compile cannot come up,
|
|
260
227
|
// and the docker output would bury the actual compile error.
|
|
261
|
-
|
|
228
|
+
const e2e = await runE2eNode({
|
|
262
229
|
projectPath,
|
|
263
230
|
featureName,
|
|
264
|
-
taskId,
|
|
265
|
-
task,
|
|
266
231
|
skipE2e,
|
|
267
232
|
freshE2e,
|
|
268
233
|
buildNode: nodes.build,
|
|
269
234
|
gate,
|
|
270
235
|
_runE2eCheck,
|
|
271
236
|
});
|
|
237
|
+
nodes.e2e = e2e.node;
|
|
272
238
|
appendE2eFeedback(feedback, nodes.e2e);
|
|
273
239
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
240
|
+
// `e2e: true` suites need the stack the e2e node just brought up (and left up).
|
|
241
|
+
if (stackNodes.length > 0) {
|
|
242
|
+
nodes.tests = await runStackTestNodes({
|
|
243
|
+
projectPath, testsNode: nodes.tests, stackNodes, e2eNode: nodes.e2e, stackEnv: e2e.stackEnv,
|
|
244
|
+
timeout: explicitTimeout, gate, _runTestSuite,
|
|
245
|
+
});
|
|
246
|
+
appendTestsFeedback(feedback, nodes.tests);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fora de `nodes` de propósito: o status sai só dos nós, então a divergência
|
|
250
|
+
// relatório × fatias (aviso de rastreabilidade) nunca vira vermelho de correção.
|
|
251
|
+
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
283
252
|
|
|
284
253
|
const status = Object.values(nodes).some((n) => n.status === 'fail') ? 'fail' : 'pass';
|
|
285
254
|
|
|
@@ -293,8 +262,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
293
262
|
return {
|
|
294
263
|
version: 1,
|
|
295
264
|
feature: featureName,
|
|
296
|
-
task: taskId || null,
|
|
297
|
-
scope: taskId ? 'task' : 'feature',
|
|
298
265
|
stack,
|
|
299
266
|
status,
|
|
300
267
|
nodes,
|
|
@@ -308,12 +275,29 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
308
275
|
// Build node
|
|
309
276
|
// ============================================================================
|
|
310
277
|
|
|
311
|
-
|
|
278
|
+
/**
|
|
279
|
+
* A root `dotnet build` with no solution at the root fails MSB1003: build the
|
|
280
|
+
* repository's single solution instead, or say which candidates and which key decide.
|
|
281
|
+
*/
|
|
282
|
+
export function resolveDotnetBuild(projectPath, stack, paths = {}) {
|
|
283
|
+
if (!stack || !isDotnetStack(stack.toLowerCase()) || paths.backendPath) return { paths, hint: null };
|
|
284
|
+
const { target, candidates } = discoverDotnetSolution(projectPath);
|
|
285
|
+
if (target === '.') return { paths, hint: null };
|
|
286
|
+
if (target) return { paths: { ...paths, backendTarget: target }, hint: null };
|
|
287
|
+
const found = candidates.length ? `candidatos: ${candidates.join(', ')}` : 'nenhum encontrado abaixo dela';
|
|
288
|
+
return {
|
|
289
|
+
paths,
|
|
290
|
+
hint: `nenhum .sln/.slnx/.csproj na raiz (${found}): declare project.backendPath em .morph/config/config.json com a pasta da solução, relativa à raiz`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function runBuildNode({ projectPath, stack, paths: declaredPaths = {}, timeout, skipBuild, gate, _runBuildCheck }) {
|
|
312
295
|
if (skipBuild) {
|
|
313
296
|
return { status: 'skip', command: null, durationMs: 0, reason: 'requested', output: '' };
|
|
314
297
|
}
|
|
315
298
|
|
|
316
|
-
const
|
|
299
|
+
const { paths, hint } = resolveDotnetBuild(projectPath, stack, declaredPaths);
|
|
300
|
+
const steps = getBuildSteps(stack, { cwd: projectPath, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
|
|
317
301
|
if (steps.length === 0) {
|
|
318
302
|
const reason = stack ? `no build command for stack "${stack}"` : 'no stack configured';
|
|
319
303
|
return { status: 'skip', command: null, durationMs: 0, reason, output: '' };
|
|
@@ -330,7 +314,7 @@ async function runBuildNode({ projectPath, stack, paths = {}, timeout, skipBuild
|
|
|
330
314
|
command,
|
|
331
315
|
durationMs,
|
|
332
316
|
reason: result.status === 'skip' ? 'no build command for stack' : null,
|
|
333
|
-
output: result.output
|
|
317
|
+
output: [result.output, result.status === 'fail' ? hint : null].filter(Boolean).join('\n'),
|
|
334
318
|
};
|
|
335
319
|
}
|
|
336
320
|
|
|
@@ -363,8 +347,8 @@ function appendBuildFeedback(feedback, buildNode) {
|
|
|
363
347
|
* `source`, `environment` (null outside a container) and `runs[]`.
|
|
364
348
|
*/
|
|
365
349
|
async function runTestsNode({
|
|
366
|
-
projectPath, testPlan, filter, skipTests, buildNode, timeout,
|
|
367
|
-
linux = false, linuxImage = null, gate, _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
350
|
+
projectPath, testPlan, filter, skipTests, buildNode, timeout, refreshCache = false,
|
|
351
|
+
linux = false, linuxImage = null, gate, stackNodes = [], _runTestSuite, _runLinuxSuite = runLinuxSuite, _exec,
|
|
368
352
|
}) {
|
|
369
353
|
const { nodes, source } = testPlan;
|
|
370
354
|
// Kept readable for whoever only prints the string — the same join the
|
|
@@ -384,28 +368,49 @@ async function runTestsNode({
|
|
|
384
368
|
|
|
385
369
|
if (linux) {
|
|
386
370
|
const { runs, environment } = await runNodesInLinux({
|
|
387
|
-
projectPath, nodes, filter,
|
|
371
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
388
372
|
});
|
|
389
373
|
return rollUpTestRuns(runs, { command, source, environment });
|
|
390
374
|
}
|
|
391
375
|
|
|
392
376
|
const runs = [];
|
|
393
377
|
for (const node of nodes) {
|
|
378
|
+
if (node.e2e && !preDecideRun({ node, filter, projectPath, refreshCache })) {
|
|
379
|
+
stackNodes.push(node);
|
|
380
|
+
runs.push(emptyRunEntry(node));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
394
383
|
runs.push(await runOneTestNode({
|
|
395
|
-
projectPath, node, filter,
|
|
384
|
+
projectPath, node, filter, timeout, refreshCache, gate, _runTestSuite,
|
|
396
385
|
}));
|
|
397
386
|
}
|
|
398
387
|
|
|
399
388
|
return rollUpTestRuns(runs, { command, source });
|
|
400
389
|
}
|
|
401
390
|
|
|
391
|
+
// No stack, no run: the skip carries the e2e verdict as reason AND warning, so the
|
|
392
|
+
// suite never disappears behind a green rollup.
|
|
393
|
+
async function runStackTestNodes({ projectPath, testsNode, stackNodes, e2eNode, stackEnv, timeout, gate, _runTestSuite }) {
|
|
394
|
+
const byId = new Map(stackNodes.map((node) => [node.id, node]));
|
|
395
|
+
const runs = [];
|
|
396
|
+
for (const run of testsNode.runs) {
|
|
397
|
+
const node = byId.get(run.id);
|
|
398
|
+
if (!node) { runs.push(run); continue; }
|
|
399
|
+
if (e2eNode.status !== 'pass') {
|
|
400
|
+
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`;
|
|
401
|
+
runs.push({ ...run, reason, warnings: [reason] });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
runs.push(await runOneTestNode({ projectPath, node, filter: null, timeout, gate, env: stackEnv, _runTestSuite }));
|
|
405
|
+
}
|
|
406
|
+
return rollUpTestRuns(runs, { command: testsNode.command, source: testsNode.source, environment: testsNode.environment });
|
|
407
|
+
}
|
|
408
|
+
|
|
402
409
|
/**
|
|
403
410
|
* Run the executable nodes inside ONE Linux container and judge them with the
|
|
404
411
|
* SAME rules the local path uses.
|
|
405
412
|
*
|
|
406
|
-
* The pre-decisions (invalid config, `--filter`, a node
|
|
407
|
-
* scope, an e2e node at task scope)
|
|
408
|
-
* happen HERE, before the container exists: a node that must not run must not
|
|
413
|
+
* The pre-decisions (invalid config, `--filter`, the eval pre-flight) happen HERE, before the container exists: a node that must not run must not
|
|
409
414
|
* be written into the container script either, or the flag would quietly run
|
|
410
415
|
* what the user filtered out. What the container returns — an exit code and an
|
|
411
416
|
* output — is then handed to `judgeRun()`, the same function the host path
|
|
@@ -415,12 +420,12 @@ async function runTestsNode({
|
|
|
415
420
|
* bind mount: see linux-runner.js. That is the whole reason this path exists.
|
|
416
421
|
*/
|
|
417
422
|
async function runNodesInLinux({
|
|
418
|
-
projectPath, nodes, filter,
|
|
423
|
+
projectPath, nodes, filter, timeout, linuxImage, gate, _runLinuxSuite, _exec,
|
|
419
424
|
}) {
|
|
420
425
|
const decided = new Map();
|
|
421
426
|
const toRun = [];
|
|
422
427
|
for (const node of nodes) {
|
|
423
|
-
const pre = preDecideRun({ node, filter,
|
|
428
|
+
const pre = preDecideRun({ node, filter, projectPath });
|
|
424
429
|
if (pre) decided.set(node.id, pre);
|
|
425
430
|
else toRun.push(node);
|
|
426
431
|
}
|
|
@@ -471,9 +476,9 @@ async function runNodesInLinux({
|
|
|
471
476
|
* from the report is the defect this whole feature exists to close.
|
|
472
477
|
*/
|
|
473
478
|
async function runOneTestNode({
|
|
474
|
-
projectPath, node, filter,
|
|
479
|
+
projectPath, node, filter, timeout, refreshCache = false, gate, env = null, _runTestSuite,
|
|
475
480
|
}) {
|
|
476
|
-
const decided = preDecideRun({ node, filter,
|
|
481
|
+
const decided = preDecideRun({ node, filter, projectPath, refreshCache });
|
|
477
482
|
if (decided) return decided;
|
|
478
483
|
|
|
479
484
|
// The eval channel: the CLI DICTATES where the cache, the dataset and the
|
|
@@ -491,8 +496,9 @@ async function runOneTestNode({
|
|
|
491
496
|
|
|
492
497
|
gate.enter('tests');
|
|
493
498
|
const start = Date.now();
|
|
499
|
+
const childEnv = env || evalChannel ? { ...(env || {}), ...(evalChannel?.env || {}) } : null;
|
|
494
500
|
const result = await _runTestSuite(projectPath, {
|
|
495
|
-
timeout, node, ...(
|
|
501
|
+
timeout, node, ...(childEnv ? { env: childEnv } : {}),
|
|
496
502
|
});
|
|
497
503
|
const entry = judgeRun({ node, result, durationMs: Date.now() - start });
|
|
498
504
|
if (!evalChannel) return entry;
|
|
@@ -572,11 +578,6 @@ function emptyRunEntry(node) {
|
|
|
572
578
|
reason: null,
|
|
573
579
|
warnings: [],
|
|
574
580
|
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
581
|
};
|
|
581
582
|
}
|
|
582
583
|
|
|
@@ -589,7 +590,7 @@ function emptyRunEntry(node) {
|
|
|
589
590
|
* two copies of this ordering is how a filtered node ends up invisible in one
|
|
590
591
|
* of them.
|
|
591
592
|
*/
|
|
592
|
-
function preDecideRun({ node, filter,
|
|
593
|
+
function preDecideRun({ node, filter, projectPath = '.', refreshCache = false }) {
|
|
593
594
|
const entry = emptyRunEntry(node);
|
|
594
595
|
|
|
595
596
|
// The resolver already decided: invalid config (fail-closed, D4) or an
|
|
@@ -606,26 +607,12 @@ function preDecideRun({ node, filter, taskId, projectPath = '.', refreshCache =
|
|
|
606
607
|
return { ...entry, reason: `filtered out (--filter ${filter.raw})` };
|
|
607
608
|
}
|
|
608
609
|
|
|
609
|
-
// The task's group scoped this node out (#87). AFTER the filter on purpose:
|
|
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
610
|
// The eval pre-flight lives HERE, beside the other pre-decisions, so the host
|
|
622
611
|
// loop and the `--linux` path refuse for the same reason with the same words.
|
|
623
612
|
// 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.
|
|
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.
|
|
613
|
+
// cache is a green gate above a verification that never happened.
|
|
627
614
|
if (node.runner === EVAL_RUNNER) {
|
|
628
|
-
const pre = evalPreflight(projectPath, node, {
|
|
615
|
+
const pre = evalPreflight(projectPath, node, { refresh: refreshCache });
|
|
629
616
|
if (pre.status) return { ...entry, status: pre.status, reason: pre.reason };
|
|
630
617
|
}
|
|
631
618
|
|
|
@@ -668,12 +655,6 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
668
655
|
const parsed = parseTestCount(result.output, node.runner);
|
|
669
656
|
const count = parsed ? parsed.total : null;
|
|
670
657
|
|
|
671
|
-
// A narrowed run is PARTIAL by construction (#87): it has a verdict of its
|
|
672
|
-
// own, and the count rule below never sees it.
|
|
673
|
-
if (entry.selection?.mode === 'narrowed') {
|
|
674
|
-
return judgeNarrowedRun({ entry, node, result, durationMs, count });
|
|
675
|
-
}
|
|
676
|
-
|
|
677
658
|
// An eval node's unit is the SCENARIO (#110 part 2): its baseline is the
|
|
678
659
|
// summary's scenario count, and `judgeEvalRun` is the one that compares it.
|
|
679
660
|
// Comparing here too, with the extractor's count of xunit cases, failed a
|
|
@@ -707,68 +688,6 @@ function judgeRun({ node, result, durationMs }) {
|
|
|
707
688
|
};
|
|
708
689
|
}
|
|
709
690
|
|
|
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
691
|
/**
|
|
773
692
|
* Aggregate the per-node runs into the single node `verify` publishes.
|
|
774
693
|
*
|
|
@@ -898,22 +817,12 @@ function refreshCacheProblem(projectPath, testPlan, filter, { skipTests }) {
|
|
|
898
817
|
* never created — the framework repository has none on purpose (creating it
|
|
899
818
|
* breaks `doctor --full`), so the honest answer is a named usage error.
|
|
900
819
|
*
|
|
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
820
|
* @param {string} projectPath
|
|
908
821
|
* @param {{ nodes: Array, source: string }} testPlan
|
|
909
|
-
* @param {{ skipTests: boolean
|
|
822
|
+
* @param {{ skipTests: boolean }} opts
|
|
910
823
|
* @returns {string|null}
|
|
911
824
|
*/
|
|
912
|
-
function baselineTargetProblem(projectPath, testPlan, { skipTests
|
|
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
|
-
}
|
|
825
|
+
function baselineTargetProblem(projectPath, testPlan, { skipTests }) {
|
|
917
826
|
if (skipTests) {
|
|
918
827
|
return '--record-baseline com --skip-tests não mede nada — uma baseline vem de uma execução, não de uma omissão';
|
|
919
828
|
}
|
|
@@ -957,12 +866,7 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
957
866
|
);
|
|
958
867
|
}
|
|
959
868
|
|
|
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
|
-
);
|
|
869
|
+
return recordTestBaselines(projectPath, runs.map((r) => ({ id: r.id, count: r.count })));
|
|
966
870
|
}
|
|
967
871
|
|
|
968
872
|
// ============================================================================
|
|
@@ -1035,53 +939,60 @@ function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
|
1035
939
|
// E2E node
|
|
1036
940
|
// ============================================================================
|
|
1037
941
|
|
|
1038
|
-
async function runE2eNode({ projectPath, featureName,
|
|
942
|
+
async function runE2eNode({ projectPath, featureName, skipE2e, freshE2e, buildNode, gate, _runE2eCheck }) {
|
|
1039
943
|
if (buildNode.status === 'fail') {
|
|
1040
944
|
return {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
945
|
+
node: {
|
|
946
|
+
status: 'skip',
|
|
947
|
+
command: null,
|
|
948
|
+
durationMs: 0,
|
|
949
|
+
reason: 'build failed — the stack could not come up anyway',
|
|
950
|
+
output: '',
|
|
951
|
+
},
|
|
952
|
+
stackEnv: null,
|
|
1046
953
|
};
|
|
1047
954
|
}
|
|
1048
955
|
|
|
1049
956
|
const config = readConfig(projectPath);
|
|
1050
|
-
const taskGroup = task?.group ?? null;
|
|
1051
957
|
const gitCommonDir = resolveGitCommonDir(projectPath);
|
|
958
|
+
// The port block matters MORE here than in `e2e up`: this is the path that
|
|
959
|
+
// runs in the loop, and two worktrees verifying at once would otherwise get
|
|
960
|
+
// distinct container names and then collide on host port 3000 — a red gate
|
|
961
|
+
// over an infrastructure clash instead of a defect.
|
|
962
|
+
const env = buildComposeEnv({ cwd: projectPath, config, featureName, gitCommonDir });
|
|
1052
963
|
|
|
1053
964
|
const start = Date.now();
|
|
1054
965
|
const result = _runE2eCheck(projectPath, {
|
|
1055
966
|
config,
|
|
1056
967
|
featureName,
|
|
1057
|
-
taskGroup,
|
|
1058
|
-
scope: taskId ? 'task' : 'feature',
|
|
1059
968
|
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 }),
|
|
969
|
+
env,
|
|
1065
970
|
skipRequested: skipE2e,
|
|
1066
971
|
forceFresh: freshE2e,
|
|
1067
972
|
beforeHeavy: () => gate.enter('e2e'),
|
|
973
|
+
countSpecs: (output) => parseTestCount(output, 'playwright')?.total ?? null,
|
|
1068
974
|
// The stack stays up between verifies on purpose: cold-starting a .NET
|
|
1069
|
-
// container per
|
|
975
|
+
// container per verify is the difference between a usable loop and one
|
|
1070
976
|
// nobody runs. `morph-spec e2e down` (and `finish`) tear it down.
|
|
1071
977
|
keepUp: true,
|
|
1072
978
|
});
|
|
1073
979
|
const durationMs = Date.now() - start;
|
|
1074
980
|
|
|
1075
981
|
return {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
982
|
+
node: {
|
|
983
|
+
status: result.status,
|
|
984
|
+
command: result.command ?? null,
|
|
985
|
+
durationMs,
|
|
986
|
+
reason: result.reason ?? null,
|
|
987
|
+
output: result.output || '',
|
|
988
|
+
// Specs Playwright counted in this run; null = no specs ran or unreadable summary.
|
|
989
|
+
count: Number.isInteger(result.count) ? result.count : null,
|
|
990
|
+
// Non-fatal findings (e.g. a compose with fixed ports at the ROOT, where
|
|
991
|
+
// nothing collides yet). Surfaced so the same compose does not fail
|
|
992
|
+
// unannounced the day the feature moves into a worktree.
|
|
993
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
994
|
+
},
|
|
995
|
+
stackEnv: { COMPOSE_PROJECT_NAME: composeProjectName(featureName), ...env },
|
|
1085
996
|
};
|
|
1086
997
|
}
|
|
1087
998
|
|
|
@@ -1096,34 +1007,6 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1096
1007
|
}
|
|
1097
1008
|
}
|
|
1098
1009
|
|
|
1099
|
-
/**
|
|
1100
|
-
* The task as verify needs it, or null when tasks.json or the task is missing.
|
|
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
1010
|
// ============================================================================
|
|
1128
1011
|
// Shared helpers
|
|
1129
1012
|
// ============================================================================
|
|
@@ -74,8 +74,7 @@ function resolveGitCommonDir(cwd) {
|
|
|
74
74
|
* treating those as different paths would make every stack look foreign.
|
|
75
75
|
*
|
|
76
76
|
* Exported as the ONE copy of that rule: `doctor.js` compares a declared test
|
|
77
|
-
* node `cwd` with a discovered `package.json` folder through it
|
|
78
|
-
* `test-scope.js` takes only the CASE of a canonical path through it (#87 N3).
|
|
77
|
+
* node `cwd` with a discovered `package.json` folder through it.
|
|
79
78
|
*/
|
|
80
79
|
export const samePathCase = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
81
80
|
const norm = (p) => samePathCase(resolve(String(p)).replace(/\\/g, '/'));
|