@polymorphism-tech/morph-spec 9.0.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- 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 +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- 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 +4 -19
- 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 +4 -50
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,32 +1,3 @@
|
|
|
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
2
|
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'fs';
|
|
32
3
|
import { join, resolve, relative, isAbsolute } from 'path';
|
|
@@ -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,15 +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
|
-
*/
|
|
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.
|
|
174
92
|
export function canonicalPath(p) {
|
|
175
93
|
try {
|
|
176
94
|
return realpathSync.native(p);
|
|
@@ -179,14 +97,6 @@ export function canonicalPath(p) {
|
|
|
179
97
|
}
|
|
180
98
|
}
|
|
181
99
|
|
|
182
|
-
/**
|
|
183
|
-
* Absolute paths of the git worktrees linked to this repo, excluding the tree
|
|
184
|
-
* being verified. Fail-open — a repo without git (or a git that errors) yields
|
|
185
|
-
* an empty list, and discovery proceeds on the convention filter alone.
|
|
186
|
-
*
|
|
187
|
-
* @param {string} projectPath
|
|
188
|
-
* @returns {string[]}
|
|
189
|
-
*/
|
|
190
100
|
function listLinkedWorktreePaths(projectPath) {
|
|
191
101
|
let out;
|
|
192
102
|
try {
|
|
@@ -208,61 +118,24 @@ function listLinkedWorktreePaths(projectPath) {
|
|
|
208
118
|
.filter((p) => p && p !== self);
|
|
209
119
|
}
|
|
210
120
|
|
|
211
|
-
/**
|
|
212
|
-
* True when `child` is `parent` or lives under it.
|
|
213
|
-
*
|
|
214
|
-
* Exported because test-nodes.js asks the SAME question of a declared `cwd`
|
|
215
|
-
* ("does this node point outside the tree under verification?"). One spelling
|
|
216
|
-
* of containment, one place to fix it.
|
|
217
|
-
*
|
|
218
|
-
* @param {string} child
|
|
219
|
-
* @param {string} parent
|
|
220
|
-
* @returns {boolean}
|
|
221
|
-
*/
|
|
121
|
+
/** True when `child` is `parent` or lives under it. */
|
|
222
122
|
export function isInside(child, parent) {
|
|
223
123
|
const rel = relative(parent, child);
|
|
224
124
|
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
225
125
|
}
|
|
226
126
|
|
|
227
|
-
/**
|
|
228
|
-
* Read `.morph/config/config.json`, or null when it is absent or malformed.
|
|
229
|
-
*
|
|
230
|
-
* Exported so test-nodes.js reads the config through the SAME door: a second
|
|
231
|
-
* reader would be a second place for "absent" and "malformed" to drift apart —
|
|
232
|
-
* and both must keep meaning "fall back", never "fail".
|
|
233
|
-
*
|
|
234
|
-
* @param {string} projectPath
|
|
235
|
-
* @returns {object|null}
|
|
236
|
-
*/
|
|
127
|
+
/** The parsed config, or null when absent OR malformed — both mean "fall back", never "fail". */
|
|
237
128
|
export function readMorphConfig(projectPath) {
|
|
238
129
|
const configPath = join(projectPath, CONFIG_FILE_NAME);
|
|
239
130
|
if (!existsSync(configPath)) return null;
|
|
240
131
|
try {
|
|
241
132
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
242
133
|
} catch {
|
|
243
|
-
// Malformed config — treated as absent by every caller
|
|
244
134
|
return null;
|
|
245
135
|
}
|
|
246
136
|
}
|
|
247
137
|
|
|
248
|
-
/**
|
|
249
|
-
* Resolve the timeout budget for ONE test node.
|
|
250
|
-
*
|
|
251
|
-
* Precedence: explicit override (`morph-spec verify --timeout`) > the node's
|
|
252
|
-
* own `timeoutMs` > `config.project.testTimeoutMs` > default scaled by the
|
|
253
|
-
* number of test projects the node's command chains.
|
|
254
|
-
*
|
|
255
|
-
* The per-node ceiling sits above the project-wide one because the derived
|
|
256
|
-
* default (120s x chained projects) is shaped for `dotnet test`: it is hostile
|
|
257
|
-
* to a Node suite that takes twelve minutes, and a project with both runners
|
|
258
|
-
* cannot serve them with a single number. It sits BELOW the CLI override
|
|
259
|
-
* because what the user typed is the most explicit statement in the room.
|
|
260
|
-
*
|
|
261
|
-
* @param {string} projectPath
|
|
262
|
-
* @param {import('./test-nodes.js').TestNode} node - The node about to run
|
|
263
|
-
* @param {number} [override] - Explicit override in ms, if the caller has one
|
|
264
|
-
* @returns {number} Timeout in ms
|
|
265
|
-
*/
|
|
138
|
+
/** Precedence: CLI override > node `timeoutMs` > config `testTimeoutMs` > default × chained projects. */
|
|
266
139
|
export function resolveTestTimeout(projectPath, node, override) {
|
|
267
140
|
if (Number.isFinite(override) && override > 0) return override;
|
|
268
141
|
|
|
@@ -274,11 +147,7 @@ export function resolveTestTimeout(projectPath, node, override) {
|
|
|
274
147
|
return DEFAULT_TEST_TIMEOUT_PER_PROJECT * Math.max(1, node?.projectCount ?? 1);
|
|
275
148
|
}
|
|
276
149
|
|
|
277
|
-
|
|
278
|
-
* Count the test runs that reported a summary before the command was cut off.
|
|
279
|
-
* Heuristic by construction (the summary line is localized), and used only to
|
|
280
|
-
* name WHICH link of the chain was still running when the clock ran out.
|
|
281
|
-
*/
|
|
150
|
+
// Heuristic: only names WHICH chain link was running when the clock ran out.
|
|
282
151
|
function countCompletedRuns(output) {
|
|
283
152
|
const matches = (output || '').match(
|
|
284
153
|
/(?:^|\n)\s*(?:Aprovado|Passed|Failed|Reprovado|Com falha|Erro)!/gi
|
|
@@ -286,27 +155,9 @@ function countCompletedRuns(output) {
|
|
|
286
155
|
return matches ? matches.length : 0;
|
|
287
156
|
}
|
|
288
157
|
|
|
289
|
-
// ============================================================================
|
|
290
|
-
// Prebuild of the test projects, and the `--no-build` refusal (T5 / UC-08)
|
|
291
|
-
// ============================================================================
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* `--no-build` as a standalone token. Anchored on whitespace so `--no-build-x`
|
|
295
|
-
* (or a path that happens to contain the word) never matches.
|
|
296
|
-
*/
|
|
297
158
|
const NO_BUILD_FLAG = /(^|\s)--no-build(\s|$)/;
|
|
298
159
|
|
|
299
|
-
|
|
300
|
-
* The `.csproj` targets a command names, quotes removed and duplicates dropped.
|
|
301
|
-
*
|
|
302
|
-
* Finding none yields `[]`, and `[]` is the SAFE answer, not a shrug: it means
|
|
303
|
-
* nothing was compiled here, which is precisely the condition that makes a
|
|
304
|
-
* `--no-build` in the same command a refusal. Refusing is cheap; approving a
|
|
305
|
-
* stale assembly is what F29 cost.
|
|
306
|
-
*
|
|
307
|
-
* @param {string|null|undefined} command
|
|
308
|
-
* @returns {string[]}
|
|
309
|
-
*/
|
|
160
|
+
// `[]` is the safe answer: nothing compiled here, so a `--no-build` in the same command is refused.
|
|
310
161
|
export function extractCsprojTargets(command) {
|
|
311
162
|
if (typeof command !== 'string') return [];
|
|
312
163
|
const found = [];
|
|
@@ -318,15 +169,7 @@ export function extractCsprojTargets(command) {
|
|
|
318
169
|
return [...new Set(found)];
|
|
319
170
|
}
|
|
320
171
|
|
|
321
|
-
/**
|
|
322
|
-
* Which test projects this node compiles before running: what it DECLARED, or
|
|
323
|
-
* else what its command names. Only `runner: 'dotnet'` has a prebuild at all —
|
|
324
|
-
* a vitest node has nothing to compile, and pretending otherwise would invent
|
|
325
|
-
* a build step nobody asked for.
|
|
326
|
-
*
|
|
327
|
-
* @param {import('./test-nodes.js').TestNode} node
|
|
328
|
-
* @returns {string[]}
|
|
329
|
-
*/
|
|
172
|
+
/** Declared `build`, else the csproj the command names; only `runner: 'dotnet'` prebuilds. */
|
|
330
173
|
export function resolveTestBuildTargets(node) {
|
|
331
174
|
if (node?.runner !== 'dotnet') return [];
|
|
332
175
|
if (Array.isArray(node.build) && node.build.length > 0) return node.build;
|
|
@@ -355,7 +198,6 @@ export function discoverDotnetBuildTargets(node, cwd) {
|
|
|
355
198
|
return only ? [dir === '.' ? only : join(dir, only).replace(/\\/g, '/')] : [];
|
|
356
199
|
}
|
|
357
200
|
|
|
358
|
-
/** The one solution among `files`, else the one project; `null` when `dotnet build` would have to guess. */
|
|
359
201
|
function singleDotnetBuildable(files) {
|
|
360
202
|
const solutions = files.filter((f) => /\.slnx?$/i.test(f));
|
|
361
203
|
const projects = files.filter((f) => /\.csproj$/i.test(f));
|
|
@@ -380,12 +222,8 @@ function dotnetBuildablesUnder(root, rel = '', depth = 0, found = []) {
|
|
|
380
222
|
}
|
|
381
223
|
|
|
382
224
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* looking inside worktree containers, node_modules, bin or obj.
|
|
386
|
-
*
|
|
387
|
-
* @returns {{ target: string|null, candidates: string[] }} `target` relative to
|
|
388
|
-
* `root` (`.` when the root already has one); `null` for zero or several, listed.
|
|
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`).
|
|
389
227
|
*/
|
|
390
228
|
export function discoverDotnetSolution(root) {
|
|
391
229
|
let rootEntries;
|
|
@@ -402,17 +240,8 @@ function isDirectory(path) {
|
|
|
402
240
|
try { return statSync(path).isDirectory(); } catch { return false; }
|
|
403
241
|
}
|
|
404
242
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
* resolves for a .NET stack — the string "dotnet build" is not typed a second
|
|
408
|
-
* time anywhere in this repository.
|
|
409
|
-
*
|
|
410
|
-
* `--no-restore` is deliberately dropped: it is correct for the ROOT build
|
|
411
|
-
* (whose restore already happened) and wrong here, because the whole reason a
|
|
412
|
-
* test project needs its own prebuild is that it may live OUTSIDE the
|
|
413
|
-
* application's solution and may therefore never have been restored at all.
|
|
414
|
-
* F29's project was exactly that project.
|
|
415
|
-
*/
|
|
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.
|
|
416
245
|
export function dotnetBuildCommandFor(target, projectPath) {
|
|
417
246
|
const base = getBuildSteps('dotnet', { cwd: projectPath || '.' })[0]?.command;
|
|
418
247
|
if (!base) return null;
|
|
@@ -421,21 +250,8 @@ export function dotnetBuildCommandFor(target, projectPath) {
|
|
|
421
250
|
return `${verb} ${subcommand} "${target}"${kept.length ? ` ${kept.join(' ')}` : ''}`;
|
|
422
251
|
}
|
|
423
252
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
* happened in this invocation — the fact the `--no-build` refusal keys on.
|
|
427
|
-
*
|
|
428
|
-
* WHEN it runs: when the node declared `build[]` (an explicit statement that
|
|
429
|
-
* these projects need compiling), or when the command carries `--no-build`
|
|
430
|
-
* (the only case where the binary would otherwise be whatever the last run
|
|
431
|
-
* left behind). A plain `dotnet test X.csproj` is NOT pre-compiled: `dotnet
|
|
432
|
-
* test` builds it itself and a compile error already reddens the node, so
|
|
433
|
-
* pre-compiling would only double the clock of the hot path — and the clock is
|
|
434
|
-
* what D13 is about.
|
|
435
|
-
*
|
|
436
|
-
* @returns {{ built: boolean, failure: object|null }} `failure` is a full
|
|
437
|
-
* runTestSuite result, ready to be returned as-is.
|
|
438
|
-
*/
|
|
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.
|
|
439
255
|
function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
|
|
440
256
|
if (node.runner !== 'dotnet') return { built: false, failure: null };
|
|
441
257
|
|
|
@@ -471,25 +287,13 @@ function runTestPrebuild({ node, cwd, exec, timeout, projectPath }) {
|
|
|
471
287
|
return { built: targets.length > 0, failure: null };
|
|
472
288
|
}
|
|
473
289
|
|
|
474
|
-
// ============================================================================
|
|
475
|
-
// How many tests actually ran (T4 / UC-08)
|
|
476
|
-
// ============================================================================
|
|
477
|
-
|
|
478
|
-
/** VSTest: one summary line per project, STARTING with the verdict. */
|
|
479
290
|
const VSTEST_SUMMARY = /^\s*(?:Aprovado|Passed|Com falha|Failed|Reprovado|Erro)!/i;
|
|
480
|
-
|
|
481
|
-
* Microsoft.Testing.Platform: the header line, in the two locales MEASURED (pt-BR and
|
|
482
|
-
* en-US). `\S+` spans `ção` — and a mis-decoded version of it — without naming a byte.
|
|
483
|
-
*/
|
|
291
|
+
// `\S+` spans `ção`, and a mis-decoded version of it, without naming a byte.
|
|
484
292
|
const MTP_SUMMARY_HEADER = /^(?:Test run summary|Resumo da execu\S+ de teste):/;
|
|
485
293
|
const MTP_TOTAL = /^\s+total:\s*(\d+)\s*$/;
|
|
486
294
|
const MTP_FAILED = /^\s+(?:failed|falhou):\s*(\d+)\s*$/;
|
|
487
295
|
const MTP_PASSED = /^\s+(?:succeeded|conclu\S*\s+com\s+\S*xito):\s*(\d+)\s*$/;
|
|
488
296
|
|
|
489
|
-
/**
|
|
490
|
-
* The indented block under an MTP summary header. It ends at the first line that
|
|
491
|
-
* starts at column 0; a block with no `total:` line yields `total: null`.
|
|
492
|
-
*/
|
|
493
297
|
function mtpSummaryBlock(lines, from) {
|
|
494
298
|
const block = { total: null, passed: 0, failed: 0 };
|
|
495
299
|
for (let i = from; i < lines.length && /^(?:\s|$)/.test(lines[i]); i++) {
|
|
@@ -502,28 +306,9 @@ function mtpSummaryBlock(lines, from) {
|
|
|
502
306
|
return block;
|
|
503
307
|
}
|
|
504
308
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
* - **VSTest**: a line per project that starts with the verdict —
|
|
509
|
-
* `Aprovado! - Com falha: 0, Aprovado: 57, Ignorado: 1, Total: 58, …`.
|
|
510
|
-
* - **Microsoft.Testing.Platform** (xunit.v3 4.x under `global.json`
|
|
511
|
-
* `test.runner`, #115): the verdict does NOT start the line —
|
|
512
|
-
* `Test run summary: Passed!` / `Resumo da execução de teste: Aprovado!` —
|
|
513
|
-
* and the count comes in an indented block below it, with a lowercase
|
|
514
|
-
* `total:`, `failed:`/`falhou:` and `succeeded:`/`concluído com êxito:`.
|
|
515
|
-
* Measured in both locales (test/fixtures/test-output/mtp-*.txt). ONE block
|
|
516
|
-
* per `dotnet test` invocation: a solution of two projects prints a single
|
|
517
|
-
* aggregated block (mtp-solution-fail-skip.txt: `total: 6` = 4 + 2), so
|
|
518
|
-
* blocks add up only across invocations. `total` counts skipped cases, as
|
|
519
|
-
* VSTest's `Total:` does. Another locale is not read: UNKNOWN, never 0.
|
|
520
|
-
*
|
|
521
|
-
* SUM, not "the last one": a chained `a && b` command prints one summary per
|
|
522
|
-
* invocation, and reading a single one would report half a suite as the whole
|
|
523
|
-
* of it — which, against a recorded baseline, is an invented count drop.
|
|
524
|
-
* `countCompletedRuns()` above counts the VSTest lines for a different question
|
|
525
|
-
* (WHICH link was running when the clock died); this one reads their numbers.
|
|
526
|
-
*/
|
|
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.
|
|
527
312
|
function parseDotnetCount(output) {
|
|
528
313
|
const lines = output.split('\n');
|
|
529
314
|
let seen = false;
|
|
@@ -552,7 +337,6 @@ function parseDotnetCount(output) {
|
|
|
552
337
|
return seen ? sum : null;
|
|
553
338
|
}
|
|
554
339
|
|
|
555
|
-
/** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
|
|
556
340
|
function lastSummaryValue(output, key) {
|
|
557
341
|
const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
|
|
558
342
|
let match;
|
|
@@ -561,7 +345,6 @@ function lastSummaryValue(output, key) {
|
|
|
561
345
|
return value;
|
|
562
346
|
}
|
|
563
347
|
|
|
564
|
-
/** `node --test`, in either of the two reporters the runner may pick. */
|
|
565
348
|
function parseNodeTestCount(output) {
|
|
566
349
|
const total = lastSummaryValue(output, 'tests');
|
|
567
350
|
if (total === null) return null;
|
|
@@ -572,13 +355,7 @@ function parseNodeTestCount(output) {
|
|
|
572
355
|
};
|
|
573
356
|
}
|
|
574
357
|
|
|
575
|
-
|
|
576
|
-
* Vitest: `Tests 3082 passed (3082)` / `Tests 1 failed | 3081 passed (3082)`.
|
|
577
|
-
*
|
|
578
|
-
* The line must start with `Tests`, never `Test Files` — the file count sitting
|
|
579
|
-
* right above it has the same shape, and reading it would report 27 where 3082
|
|
580
|
-
* ran. A fabricated drop is worse than no count at all.
|
|
581
|
-
*/
|
|
358
|
+
// `Tests`, never `Test Files`: the file-count line above has the same shape.
|
|
582
359
|
function parseVitestCount(output) {
|
|
583
360
|
const line = output.split('\n').find((l) => /^\s*Tests\s{1,}\d/.test(l));
|
|
584
361
|
if (!line) return null;
|
|
@@ -591,7 +368,6 @@ function parseVitestCount(output) {
|
|
|
591
368
|
return { total: declaredTotal ? Number(declaredTotal[1]) : p + f, passed: p, failed: f };
|
|
592
369
|
}
|
|
593
370
|
|
|
594
|
-
/** Jest: `Tests: 2 failed, 118 passed, 120 total`. */
|
|
595
371
|
function parseJestCount(output) {
|
|
596
372
|
const line = output.split('\n').find((l) => /^\s*Tests:\s/.test(l));
|
|
597
373
|
if (!line) return null;
|
|
@@ -606,33 +382,14 @@ function parseJestCount(output) {
|
|
|
606
382
|
};
|
|
607
383
|
}
|
|
608
384
|
|
|
609
|
-
/**
|
|
610
|
-
* CSI (`ESC [ ... <final byte @-~>`, e.g. SGR color codes) and OSC (`ESC ] ...
|
|
611
|
-
* BEL|ESC\`, e.g. hyperlinks/terminal titles) escape sequences — the two
|
|
612
|
-
* shapes a captured terminal transcript actually carries (issue #110: a
|
|
613
|
-
* vitest summary piped through `execSync` kept its SGR bytes, and every count
|
|
614
|
-
* regex in this file is anchored on `^` / exact literals, so a color code
|
|
615
|
-
* sitting between "Tests" and the number silently defeated the whole line).
|
|
616
|
-
*/
|
|
617
385
|
const ANSI_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
|
|
618
386
|
|
|
619
|
-
/**
|
|
620
|
-
* Strip ANSI escape sequences (CSI/SGR and OSC) from captured process output.
|
|
621
|
-
*
|
|
622
|
-
* Non-string input is returned UNCHANGED, never coerced to `''` — the caller
|
|
623
|
-
* that matters (`parseTestCount`) already treats a non-string
|
|
624
|
-
* output as "nothing to read" on their own; making this function opinionated
|
|
625
|
-
* about that too would be a second place for the same rule to drift.
|
|
626
|
-
*
|
|
627
|
-
* @param {string} text
|
|
628
|
-
* @returns {string} `text` with escape sequences removed; unchanged if not a string
|
|
629
|
-
*/
|
|
387
|
+
/** Strips CSI/SGR and OSC sequences; non-string input is returned unchanged, never coerced. */
|
|
630
388
|
export function stripAnsi(text) {
|
|
631
389
|
if (typeof text !== 'string') return text;
|
|
632
390
|
return text.replace(ANSI_PATTERN, '');
|
|
633
391
|
}
|
|
634
392
|
|
|
635
|
-
/** Playwright: `8 passed (12.4s)` and/or `2 failed`, on their own lines. */
|
|
636
393
|
function parsePlaywrightCount(output) {
|
|
637
394
|
const passed = /^\s*(\d+)\s+passed\b/im.exec(output);
|
|
638
395
|
const failed = /^\s*(\d+)\s+failed\b/im.exec(output);
|
|
@@ -642,15 +399,10 @@ function parsePlaywrightCount(output) {
|
|
|
642
399
|
return { total: p + f, passed: p, failed: f };
|
|
643
400
|
}
|
|
644
401
|
|
|
645
|
-
/** One extractor per runner. An absent entry is an honest "I cannot read this". */
|
|
646
402
|
const COUNT_EXTRACTORS = {
|
|
647
403
|
dotnet: parseDotnetCount,
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
// by is the `scenarios` field of the fixture's summary FILE, which this
|
|
651
|
-
// function cannot see: `judgeEvalRun` replaces this count with it, or with
|
|
652
|
-
// `null` (UNKNOWN, never zero) when the file is unreadable, and `judgeRun`
|
|
653
|
-
// 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`.
|
|
654
406
|
evals: parseDotnetCount,
|
|
655
407
|
'node-test': parseNodeTestCount,
|
|
656
408
|
vitest: parseVitestCount,
|
|
@@ -660,50 +412,25 @@ const COUNT_EXTRACTORS = {
|
|
|
660
412
|
};
|
|
661
413
|
|
|
662
414
|
/**
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
* @param {string|null|undefined} output - Combined stdout+stderr of the run
|
|
666
|
-
* @param {string} runner - One of TEST_RUNNERS (see test-nodes.js)
|
|
667
|
-
* @returns {{ total: number, passed: number, failed: number }|null}
|
|
668
|
-
* **null is UNKNOWN, never zero.** A summary nobody could read is not a
|
|
669
|
-
* suite that ran zero tests: collapsing the two would manufacture a total
|
|
670
|
-
* count drop against every recorded baseline out of a missing regex. Same
|
|
671
|
-
* 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.
|
|
672
417
|
*/
|
|
673
418
|
export function parseTestCount(output, runner) {
|
|
674
419
|
if (typeof output !== 'string' || !output) return null;
|
|
675
420
|
const extractor = COUNT_EXTRACTORS[runner];
|
|
676
421
|
if (!extractor) return null;
|
|
677
|
-
//
|
|
678
|
-
// bytes — a captured terminal transcript may carry SGR color codes right
|
|
679
|
-
// inside a summary line (#110) and every regex above is anchored on exact
|
|
680
|
-
// 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.
|
|
681
423
|
return extractor(stripAnsi(output));
|
|
682
424
|
}
|
|
683
425
|
|
|
684
426
|
/**
|
|
685
|
-
*
|
|
686
|
-
* and
|
|
687
|
-
*
|
|
688
|
-
* A count that DROPS is a finding: tests that used to run no longer do, which
|
|
689
|
-
* is exactly the silent damage this feature exists to catch. A count that
|
|
690
|
-
* RISES is the normal life of a suite that grows with every task. A symmetric
|
|
691
|
-
* rule would turn each new test into a red gate and would teach the agent to
|
|
692
|
-
* delete tests in order to "fix" it — the lesson MORPH.md already paid for
|
|
693
|
-
* (a typed number goes stale on the first task).
|
|
694
|
-
*
|
|
695
|
-
* @param {number|null} count - Executed count; null is UNKNOWN
|
|
696
|
-
* @param {number|null} expectedCount - Recorded baseline; null is "none recorded"
|
|
697
|
-
* @returns {{ verdict: 'drop'|'rise'|'match'|'unknown'|'no-baseline', reason: string|null, warning: string|null }}
|
|
698
|
-
* `reason` is non-null ONLY for a drop — it is the only verdict that fails a
|
|
699
|
-
* 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.
|
|
700
429
|
*/
|
|
701
430
|
export function compareTestCount(count, expectedCount) {
|
|
702
431
|
const answer = (verdict, reason = null, warning = null) => ({ verdict, reason, warning });
|
|
703
432
|
|
|
704
|
-
//
|
|
705
|
-
// distinction has to survive here or "this suite runs zero tests" becomes
|
|
706
|
-
// inexpressible.
|
|
433
|
+
// A declared `0` is a real baseline; only null means "none recorded".
|
|
707
434
|
if (!Number.isInteger(expectedCount)) return answer('no-baseline');
|
|
708
435
|
|
|
709
436
|
if (!Number.isInteger(count)) {
|
|
@@ -724,25 +451,8 @@ export function compareTestCount(count, expectedCount) {
|
|
|
724
451
|
}
|
|
725
452
|
|
|
726
453
|
/**
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* The node is a parameter rather than something resolved here: a project has a
|
|
730
|
-
* LIST of suites (see test-nodes.js), and a runner that resolved its own single
|
|
731
|
-
* command is precisely what let 3.082 vitest tests sit outside the gate (F30).
|
|
732
|
-
* Resolution belongs to `resolveTestNodes()`; execution belongs here. One node,
|
|
733
|
-
* one command, one cwd, one clock.
|
|
734
|
-
*
|
|
735
|
-
* @param {string} [projectPath] - Project root directory (defaults to '.')
|
|
736
|
-
* @param {object} [opts]
|
|
737
|
-
* @param {import('./test-nodes.js').TestNode} opts.node - The node to run; its
|
|
738
|
-
* `cwd` is resolved against `projectPath`
|
|
739
|
-
* @param {number} [opts.timeout] - Explicit timeout in ms (highest precedence)
|
|
740
|
-
* @param {Record<string,string>} [opts.env] - Extra environment for THIS node, layered
|
|
741
|
-
* over `process.env`. The eval runner's channel (`MORPH_EVAL_*`) travels here.
|
|
742
|
-
* Absent leaves the child on the parent's environment, byte for byte — every
|
|
743
|
-
* other runner keeps the exact behaviour it had.
|
|
744
|
-
* @param {Function} [opts._execSync] - Override execSync for unit testing
|
|
745
|
-
* @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).
|
|
746
456
|
*/
|
|
747
457
|
export function runTestSuite(projectPath = '.', opts = {}) {
|
|
748
458
|
const node = opts.node;
|
|
@@ -760,17 +470,12 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
760
470
|
const projectCount = node.projectCount ?? 1;
|
|
761
471
|
const cwd = resolve(projectPath, node.cwd || '.');
|
|
762
472
|
|
|
763
|
-
//
|
|
764
|
-
// 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.
|
|
765
474
|
const prebuild = runTestPrebuild({ node, cwd, exec, timeout, projectPath });
|
|
766
475
|
if (prebuild.failure) return prebuild.failure;
|
|
767
476
|
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
// executed. The refusal keys on whether a build HAPPENED here, never on the
|
|
771
|
-
// flag alone — `--no-build` after a real build is the fast path (D11). The
|
|
772
|
-
// command itself is never rewritten: a silent fix would hide the config
|
|
773
|
-
// 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.
|
|
774
479
|
if (node.runner === 'dotnet' && NO_BUILD_FLAG.test(cmd) && !prebuild.built) {
|
|
775
480
|
return {
|
|
776
481
|
passed: false,
|
|
@@ -782,8 +487,7 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
782
487
|
};
|
|
783
488
|
}
|
|
784
489
|
|
|
785
|
-
// Layered OVER process.env
|
|
786
|
-
// 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.
|
|
787
491
|
const env = opts.env ? { ...process.env, ...opts.env } : undefined;
|
|
788
492
|
|
|
789
493
|
try {
|
|
@@ -800,9 +504,7 @@ export function runTestSuite(projectPath = '.', opts = {}) {
|
|
|
800
504
|
const output = (err.stdout || '') + (err.stderr || '')
|
|
801
505
|
|| [err.code, err.signal, err.message].filter(Boolean).join(' ');
|
|
802
506
|
|
|
803
|
-
// A timeout is NOT a red suite: the command was interrupted, so no verdict
|
|
804
|
-
// exists. Saying so by name is the whole point — the field diagnosis of
|
|
805
|
-
// 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.
|
|
806
508
|
if (err.code === 'ETIMEDOUT' || err.killed === true) {
|
|
807
509
|
const total = Math.max(1, projectCount);
|
|
808
510
|
const at = Math.min(countCompletedRuns(output) + 1, total);
|