dd-trace 5.116.0 → 5.118.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/ci/diagnose.js +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +3 -26
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +75 -13
- package/ci/test-optimization-validation/generated-test-contract.js +283 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +7 -0
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/cucumber.js +11 -12
- package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/jest.js +18 -17
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-instrumentations/src/playwright.js +3 -24
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +2 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/datadog-plugin-playwright/src/index.js +4 -1
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
- package/packages/dd-trace/src/llmobs/tagger.js +23 -3
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- package/ci/test-optimization-validation-manifest.schema.json +0 -1
|
@@ -2,236 +2,140 @@
|
|
|
2
2
|
|
|
3
3
|
/* eslint-disable no-console */
|
|
4
4
|
|
|
5
|
-
const fs = require('fs')
|
|
6
|
-
const path = require('path')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
7
|
|
|
8
|
-
const {
|
|
9
|
-
const { DD_MAJOR } = require('../../version')
|
|
10
|
-
|
|
11
|
-
const { assertApprovalDigest, getApprovalDigest } = require('./approval')
|
|
8
|
+
const { assertApprovalDigest } = require('./approval')
|
|
12
9
|
const { loadApprovedPlan } = require('./approval-artifacts')
|
|
13
|
-
|
|
14
|
-
const { runBasicReporting } = require('./scenarios/basic-reporting')
|
|
15
|
-
const { runEarlyFlakeDetection } = require('./scenarios/early-flake-detection')
|
|
16
|
-
const { runAutoTestRetries } = require('./scenarios/auto-test-retries')
|
|
17
|
-
const { runTestManagement } = require('./scenarios/test-management')
|
|
18
|
-
const { runCiWiring } = require('./scenarios/ci-wiring')
|
|
10
|
+
const { annotateCiDiscovery } = require('./ci-discovery')
|
|
19
11
|
const { cleanupGeneratedFiles } = require('./generated-files')
|
|
20
12
|
const { verifyGeneratedTestStrategy } = require('./generated-verifier')
|
|
21
|
-
const { annotateCiDiscovery } = require('./ci-discovery')
|
|
22
13
|
const { loadManifest } = require('./manifest-loader')
|
|
23
14
|
const { createManifestScaffold } = require('./manifest-scaffold')
|
|
24
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
formatExecutionPlanArtifacts,
|
|
17
|
+
getExecutionPlanPath,
|
|
18
|
+
} = require('./plan-writer')
|
|
25
19
|
const { runFrameworkPreflight } = require('./preflight-runner')
|
|
26
20
|
const { sanitizeConsoleText } = require('./redaction')
|
|
21
|
+
const {
|
|
22
|
+
annotateResults,
|
|
23
|
+
getExecutionStatus,
|
|
24
|
+
getValidationCoverage,
|
|
25
|
+
getValidatorExitCode,
|
|
26
|
+
} = require('./result-semantics')
|
|
27
27
|
const { writePendingReport, writeReport } = require('./report-writer')
|
|
28
|
+
const { getBasicCommand } = require('./runner-command')
|
|
29
|
+
const { getUnavailableExecutable } = require('./executable')
|
|
30
|
+
const { runAutoTestRetries } = require('./scenarios/auto-test-retries')
|
|
31
|
+
const { runBasicReporting } = require('./scenarios/basic-reporting')
|
|
32
|
+
const { runCiWiring } = require('./scenarios/ci-wiring')
|
|
33
|
+
const { runEarlyFlakeDetection } = require('./scenarios/early-flake-detection')
|
|
34
|
+
const { runTestManagement } = require('./scenarios/test-management')
|
|
28
35
|
const { ensureSafeDirectory } = require('./safe-files')
|
|
29
|
-
const {
|
|
30
|
-
const {
|
|
31
|
-
getStaticBlocker,
|
|
32
|
-
runStaticDiagnosis,
|
|
33
|
-
} = require('./static-diagnosis')
|
|
36
|
+
const { getStaticBlocker, runStaticDiagnosis } = require('./static-diagnosis')
|
|
34
37
|
|
|
35
38
|
const DEFAULT_MANIFEST = './dd-test-optimization-validation-manifest.json'
|
|
36
39
|
const DEFAULT_OUT = './dd-test-optimization-validation-results'
|
|
37
|
-
|
|
38
40
|
const SCENARIOS = {
|
|
39
41
|
'basic-reporting': runBasicReporting,
|
|
40
42
|
efd: runEarlyFlakeDetection,
|
|
41
43
|
atr: runAutoTestRetries,
|
|
42
44
|
'test-management': runTestManagement,
|
|
43
45
|
}
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
+
const BASIC_REPORTING = 'basic-reporting'
|
|
47
|
+
const CI_WIRING = 'ci-wiring'
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parses validator CLI arguments.
|
|
51
|
+
*
|
|
52
|
+
* @param {string[]} argv command arguments
|
|
53
|
+
* @returns {object} parsed options
|
|
54
|
+
*/
|
|
47
55
|
function parseArgs (argv) {
|
|
48
56
|
const options = {
|
|
57
|
+
approvalOverrides: [],
|
|
58
|
+
frameworks: new Set(),
|
|
59
|
+
keepTempFiles: false,
|
|
49
60
|
manifest: DEFAULT_MANIFEST,
|
|
50
61
|
out: DEFAULT_OUT,
|
|
51
|
-
frameworks: new Set(),
|
|
52
|
-
scenarios: new Set(getSelectableScenarios()),
|
|
53
62
|
requestedScenario: null,
|
|
54
|
-
|
|
63
|
+
scenarios: new Set(getSelectableScenarios()),
|
|
55
64
|
verbose: false,
|
|
56
|
-
approvalOverrides: [],
|
|
57
65
|
}
|
|
58
66
|
|
|
59
|
-
for (let
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
options.printPlan = true
|
|
95
|
-
break
|
|
96
|
-
case '--print-approval-sha256':
|
|
97
|
-
options.printApprovalSha256 = true
|
|
98
|
-
break
|
|
99
|
-
case '--approved-plan-sha256':
|
|
100
|
-
options.approvedPlanSha256 = requireValue(argv, ++i, arg)
|
|
101
|
-
break
|
|
102
|
-
case '--offline-fixture-nonce':
|
|
103
|
-
options.offlineFixtureNonce = requireValue(argv, ++i, arg)
|
|
104
|
-
break
|
|
105
|
-
case '--run-approved-plan':
|
|
106
|
-
options.runApprovedPlan = requireValue(argv, ++i, arg)
|
|
107
|
-
break
|
|
108
|
-
case '--sha256':
|
|
109
|
-
options.approvedArtifactSha256 = requireValue(argv, ++i, arg)
|
|
110
|
-
break
|
|
111
|
-
case '--help':
|
|
112
|
-
case '-h':
|
|
113
|
-
options.help = true
|
|
114
|
-
break
|
|
115
|
-
default:
|
|
116
|
-
throw new Error(`Unknown argument: ${arg}`)
|
|
67
|
+
for (let index = 0; index < argv.length; index++) {
|
|
68
|
+
const argument = argv[index]
|
|
69
|
+
if (argument === '--manifest') {
|
|
70
|
+
options.manifest = requireValue(argv, ++index, argument)
|
|
71
|
+
options.approvalOverrides.push(argument)
|
|
72
|
+
} else if (argument === '--out') {
|
|
73
|
+
options.out = requireValue(argv, ++index, argument)
|
|
74
|
+
options.approvalOverrides.push(argument)
|
|
75
|
+
} else if (argument === '--framework') {
|
|
76
|
+
options.frameworks.add(normalizeFrameworkTarget(requireValue(argv, ++index, argument)))
|
|
77
|
+
options.approvalOverrides.push(argument)
|
|
78
|
+
} else if (argument === '--scenario') {
|
|
79
|
+
options.requestedScenario = requireValue(argv, ++index, argument)
|
|
80
|
+
options.scenarios = normalizeScenarioSelection(options.requestedScenario)
|
|
81
|
+
options.approvalOverrides.push(argument)
|
|
82
|
+
} else if (argument === '--keep-temp-files') {
|
|
83
|
+
options.keepTempFiles = true
|
|
84
|
+
options.approvalOverrides.push(argument)
|
|
85
|
+
} else if (argument === '--verbose') {
|
|
86
|
+
options.verbose = true
|
|
87
|
+
options.approvalOverrides.push(argument)
|
|
88
|
+
} else if (argument === '--validate-manifest') {
|
|
89
|
+
options.validateManifest = true
|
|
90
|
+
} else if (argument === '--init-manifest') {
|
|
91
|
+
options.initManifest = true
|
|
92
|
+
} else if (argument === '--print-plan') {
|
|
93
|
+
options.printPlan = true
|
|
94
|
+
} else if (argument === '--run-approved-plan') {
|
|
95
|
+
options.runApprovedPlan = requireValue(argv, ++index, argument)
|
|
96
|
+
} else if (argument === '--sha256') {
|
|
97
|
+
options.approvedArtifactSha256 = requireValue(argv, ++index, argument)
|
|
98
|
+
} else if (argument === '--help' || argument === '-h') {
|
|
99
|
+
options.help = true
|
|
100
|
+
} else {
|
|
101
|
+
throw new Error(`Unknown argument: ${argument}`)
|
|
117
102
|
}
|
|
118
103
|
}
|
|
119
|
-
|
|
120
|
-
for (const scenario of options.scenarios) {
|
|
121
|
-
if (!getSelectableScenarios().includes(scenario)) {
|
|
122
|
-
throw new Error(`Unknown scenario "${scenario}". Expected one of: ${getSelectableScenarios().join(', ')}`)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
104
|
return options
|
|
127
105
|
}
|
|
128
106
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
function printHelp () {
|
|
137
|
-
console.log(`Usage:
|
|
138
|
-
node ci/validate-test-optimization.js [options]
|
|
139
|
-
|
|
140
|
-
Options:
|
|
141
|
-
--manifest <path> Manifest path. Defaults to ${DEFAULT_MANIFEST}
|
|
142
|
-
--out <path> Output directory. Defaults to ${DEFAULT_OUT}
|
|
143
|
-
--framework <id> Run one framework entry. Can be repeated. A trailing ":" is ignored.
|
|
144
|
-
A framework kind such as "vitest" runs all matching Vitest entries.
|
|
145
|
-
--scenario <name> Run one scenario: ${getSelectableScenarios().join(', ')}
|
|
146
|
-
--keep-temp-files Leave generated validation files in place.
|
|
147
|
-
--verbose Print command progress.
|
|
148
|
-
--validate-manifest Validate the manifest and exit without running project code.
|
|
149
|
-
--init-manifest Create a schema-valid manifest scaffold without running project code.
|
|
150
|
-
--print-plan Write the plan and approval artifacts without running project code.
|
|
151
|
-
--run-approved-plan Run the exact approval.json produced by --print-plan.
|
|
152
|
-
--sha256 <digest> Require approval.json and reconstructed current inputs to match this SHA-256.
|
|
153
|
-
--help Show this help.
|
|
154
|
-
`)
|
|
155
|
-
}
|
|
156
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Runs the validator CLI.
|
|
109
|
+
*
|
|
110
|
+
* @param {string[]} argv command arguments
|
|
111
|
+
* @returns {Promise<void>} completion
|
|
112
|
+
*/
|
|
157
113
|
async function main (argv) {
|
|
114
|
+
let activeManifest
|
|
115
|
+
let activeOut
|
|
116
|
+
let cleanupOutcome = { status: 'not_started' }
|
|
117
|
+
|
|
158
118
|
try {
|
|
159
119
|
const options = parseArgs(argv)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (options.initManifest) {
|
|
166
|
-
const manifestPath = path.resolve(options.manifest)
|
|
167
|
-
if (path.dirname(manifestPath) !== process.cwd()) {
|
|
168
|
-
throw new Error('The generated manifest must be stored directly in the current repository root.')
|
|
169
|
-
}
|
|
170
|
-
const manifest = createManifestScaffold({ root: process.cwd(), frameworks: options.frameworks })
|
|
171
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
|
|
172
|
-
console.log(sanitizeConsoleText(
|
|
173
|
-
`Created validation manifest scaffold without running project code: ${manifestPath}\n` +
|
|
174
|
-
'This scaffold is already schema-valid; preserve its command boilerplate. Review the selected test commands ' +
|
|
175
|
-
'and CI files listed in ciDiscovery, record one replayable CI test step when available, then run ' +
|
|
176
|
-
'--validate-manifest and follow its field-specific errors.'
|
|
177
|
-
))
|
|
178
|
-
return
|
|
179
|
-
}
|
|
120
|
+
assertCompatibleModes(options)
|
|
121
|
+
if (options.help) return printHelp()
|
|
122
|
+
if (options.initManifest) return initializeManifest(options)
|
|
180
123
|
|
|
181
124
|
if (options.runApprovedPlan) applyApprovedPlanOptions(options)
|
|
182
|
-
|
|
183
125
|
const manifest = loadManifest(options.manifest)
|
|
184
|
-
|
|
185
|
-
const out = validateOutputPath(manifest, options.out)
|
|
186
|
-
const approvalManifest = getApprovalManifest(manifest, options.frameworks)
|
|
187
|
-
formatExecutionPlan({
|
|
188
|
-
manifest: approvalManifest,
|
|
189
|
-
out,
|
|
190
|
-
selectedFrameworkIds: options.frameworks.size > 0
|
|
191
|
-
? approvalManifest.frameworks.map(framework => framework.id)
|
|
192
|
-
: [],
|
|
193
|
-
requestedScenario: options.requestedScenario,
|
|
194
|
-
keepTempFiles: options.keepTempFiles,
|
|
195
|
-
verbose: options.verbose,
|
|
196
|
-
})
|
|
197
|
-
console.log(sanitizeConsoleText(
|
|
198
|
-
'[test-optimization-validator-agent] Customer approval summary written to ' +
|
|
199
|
-
`${getApprovalSummaryPath(out)}. Read that file and send its complete contents in a user-facing message ` +
|
|
200
|
-
`before requesting approval. Detailed audit information is in ${getExecutionPlanPath(out)}. ` +
|
|
201
|
-
'The approval command is available only in the summary.'
|
|
202
|
-
))
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
if (options.printApprovalSha256) {
|
|
206
|
-
if (!options.offlineFixtureNonce) {
|
|
207
|
-
throw new Error('--print-approval-sha256 requires the --offline-fixture-nonce value shown in the plan.')
|
|
208
|
-
}
|
|
209
|
-
const out = validateOutputPath(manifest, options.out)
|
|
210
|
-
const approvalManifest = getApprovalManifest(manifest, options.frameworks)
|
|
211
|
-
console.log(getApprovalDigest({
|
|
212
|
-
manifest: approvalManifest,
|
|
213
|
-
out,
|
|
214
|
-
selectedFrameworkIds: options.frameworks.size > 0
|
|
215
|
-
? approvalManifest.frameworks.map(framework => framework.id)
|
|
216
|
-
: [],
|
|
217
|
-
requestedScenario: options.requestedScenario,
|
|
218
|
-
offlineFixtureNonce: options.offlineFixtureNonce,
|
|
219
|
-
keepTempFiles: options.keepTempFiles,
|
|
220
|
-
verbose: options.verbose,
|
|
221
|
-
}))
|
|
222
|
-
return
|
|
223
|
-
}
|
|
126
|
+
|
|
224
127
|
if (options.validateManifest) {
|
|
225
128
|
console.log(sanitizeConsoleText(`Validation manifest is valid: ${manifest.__path}`))
|
|
226
129
|
return
|
|
227
130
|
}
|
|
131
|
+
if (options.printPlan) return printPlan(manifest, options)
|
|
228
132
|
if (!options.approvedPlanSha256 || !options.offlineFixtureNonce) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
'Live validation requires --run-approved-plan and --sha256 from a reviewed --print-plan result. Render and ' +
|
|
231
|
-
'approve a fresh execution plan first.'
|
|
232
|
-
)
|
|
133
|
+
throw new Error('Live validation requires the checksum-bound command produced by --print-plan.')
|
|
233
134
|
}
|
|
135
|
+
|
|
234
136
|
const out = validateOutputPath(manifest, options.out)
|
|
137
|
+
activeManifest = manifest
|
|
138
|
+
activeOut = out
|
|
235
139
|
options.repositoryRoot = manifest.repository.root
|
|
236
140
|
const selectedFrameworks = filterFrameworks(manifest.frameworks, options.frameworks)
|
|
237
141
|
const approvalManifest = getApprovalManifest(manifest, options.frameworks)
|
|
@@ -248,172 +152,233 @@ async function main (argv) {
|
|
|
248
152
|
})
|
|
249
153
|
options.requireExecutableApproval = true
|
|
250
154
|
ensureSafeDirectory(manifest.repository.root, out, 'validation output directory', { allowRootSymlink: true })
|
|
251
|
-
|
|
155
|
+
writePendingReport({ manifest, out })
|
|
156
|
+
|
|
252
157
|
const staticDiagnosis = runStaticDiagnosis({ manifest, out })
|
|
253
158
|
annotateCiDiscovery({ manifest, diagnosis: staticDiagnosis.report })
|
|
254
|
-
|
|
255
159
|
const results = []
|
|
256
|
-
const runnableFrameworks = []
|
|
257
160
|
|
|
258
161
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
for (const framework of frameworks) {
|
|
263
|
-
if (framework.status !== 'runnable') {
|
|
264
|
-
results.push(getFrameworkStatusResult(framework))
|
|
265
|
-
continue
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const staticBlocker = getStaticBlocker(framework, staticDiagnosis.report)
|
|
269
|
-
if (staticBlocker) {
|
|
270
|
-
results.push(getStaticFailure(framework, staticBlocker, staticDiagnosis.reportPath))
|
|
271
|
-
continue
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
liveReadyFrameworks.push(framework)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
for (const framework of liveReadyFrameworks) {
|
|
278
|
-
// Setup commands are project preparation, not Test Optimization signal collection.
|
|
279
|
-
if (framework.setup?.commands?.length > 0) logPhaseStart(framework, 'Project setup')
|
|
162
|
+
for (const framework of selectedFrameworks) {
|
|
163
|
+
// Each framework is independent. A setup blocker must not discard useful results from another adapter.
|
|
280
164
|
// eslint-disable-next-line no-await-in-loop
|
|
281
|
-
|
|
282
|
-
if (framework.setup?.commands?.length > 0) {
|
|
283
|
-
logPhaseComplete(framework, 'Project setup', setup.ok ? 'pass' : setup.failure?.status)
|
|
284
|
-
}
|
|
285
|
-
if (!setup.ok) {
|
|
286
|
-
results.push(setup.failure)
|
|
287
|
-
continue
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
runnableFrameworks.push(framework)
|
|
165
|
+
await validateFramework({ framework, manifest, options, out, results, staticDiagnosis })
|
|
291
166
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// eslint-disable-next-line no-await-in-loop
|
|
298
|
-
const preflight = await runFrameworkPreflight({ framework, out, options })
|
|
299
|
-
logPhaseComplete(
|
|
300
|
-
framework,
|
|
301
|
-
'Test execution without Datadog',
|
|
302
|
-
preflight.ok ? 'pass' : preflight.failure?.status
|
|
303
|
-
)
|
|
304
|
-
// Scenarios intentionally run in order so each one can use an isolated offline fixture.
|
|
305
|
-
if (preflight.ok) {
|
|
306
|
-
logPhaseStart(framework, 'Basic Reporting')
|
|
307
|
-
// eslint-disable-next-line no-await-in-loop
|
|
308
|
-
basicResult = await SCENARIOS[BASIC_REPORTING_SCENARIO]({ manifest, framework, out, options })
|
|
309
|
-
logPhaseComplete(framework, 'Basic Reporting', basicResult.status)
|
|
310
|
-
} else {
|
|
311
|
-
basicResult = preflight.failure
|
|
312
|
-
}
|
|
313
|
-
results.push(basicResult)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (options.scenarios.has(CI_WIRING_SCENARIO)) {
|
|
317
|
-
if (basicResult && basicResult.status !== 'pass') {
|
|
318
|
-
results.push(getSkippedCiWiringAfterBasicFailure(framework, basicResult))
|
|
319
|
-
} else {
|
|
320
|
-
// CI wiring runs after Basic Reporting proves this framework can report when initialized directly.
|
|
321
|
-
logPhaseStart(framework, 'CI wiring')
|
|
322
|
-
// eslint-disable-next-line no-await-in-loop
|
|
323
|
-
const ciWiringResult = await runCiWiring({ manifest, framework, out, options, basicResult })
|
|
324
|
-
results.push(ciWiringResult)
|
|
325
|
-
logPhaseComplete(framework, 'CI wiring', ciWiringResult.status)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const advancedScenarios = getAdvancedScenarios(options.scenarios)
|
|
330
|
-
if (basicResult && basicResult.status !== 'pass') {
|
|
331
|
-
for (const scenario of advancedScenarios) {
|
|
332
|
-
results.push(getSkippedAfterBasicFailure(framework, scenario, basicResult))
|
|
333
|
-
}
|
|
334
|
-
continue
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (advancedScenarios.length > 0) {
|
|
338
|
-
logPhaseStart(framework, 'Temporary test verification')
|
|
339
|
-
// eslint-disable-next-line no-await-in-loop
|
|
340
|
-
const generatedVerification = await verifyGeneratedTestStrategy({ framework, out, options })
|
|
341
|
-
logPhaseComplete(
|
|
342
|
-
framework,
|
|
343
|
-
'Temporary test verification',
|
|
344
|
-
generatedVerification.ok ? 'pass' : generatedVerification.failure?.status
|
|
345
|
-
)
|
|
346
|
-
if (!generatedVerification.ok) {
|
|
347
|
-
results.push(generatedVerification.failure)
|
|
348
|
-
for (const scenario of advancedScenarios) {
|
|
349
|
-
results.push(getSkippedAfterGeneratedVerificationFailure(
|
|
350
|
-
framework,
|
|
351
|
-
scenario,
|
|
352
|
-
generatedVerification.failure
|
|
353
|
-
))
|
|
354
|
-
}
|
|
355
|
-
continue
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
for (const scenario of advancedScenarios) {
|
|
360
|
-
const runScenario = SCENARIOS[scenario]
|
|
361
|
-
// Scenarios intentionally run in order so each one can use an isolated offline fixture.
|
|
362
|
-
logPhaseStart(framework, getScenarioDisplayName(scenario))
|
|
363
|
-
// eslint-disable-next-line no-await-in-loop
|
|
364
|
-
const result = await runScenario({ manifest, framework, out, options })
|
|
365
|
-
results.push(result)
|
|
366
|
-
logPhaseComplete(framework, getScenarioDisplayName(scenario), result.status)
|
|
167
|
+
} finally {
|
|
168
|
+
try {
|
|
169
|
+
cleanupOutcome = await cleanupGeneratedFiles(manifest, { keep: options.keepTempFiles })
|
|
170
|
+
if (cleanupOutcome.status === 'incomplete') {
|
|
171
|
+
results.push(getCleanupFailure(undefined, cleanupOutcome))
|
|
367
172
|
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
cleanupOutcome = { errorCount: 1, status: 'incomplete' }
|
|
175
|
+
results.push(getCleanupFailure(error, cleanupOutcome))
|
|
368
176
|
}
|
|
369
|
-
} finally {
|
|
370
|
-
await cleanupGeneratedFiles(manifest, { keep: options.keepTempFiles })
|
|
371
177
|
}
|
|
372
178
|
|
|
373
|
-
|
|
374
|
-
const
|
|
179
|
+
addMissingResults(results, selectedFrameworks, options.scenarios)
|
|
180
|
+
const annotatedResults = annotateResults(results)
|
|
181
|
+
const executionStatus = getExecutionStatus(annotatedResults)
|
|
182
|
+
const validatorExitCode = getValidatorExitCode(annotatedResults, executionStatus)
|
|
375
183
|
await writeReport({
|
|
376
184
|
manifest,
|
|
377
|
-
results,
|
|
378
185
|
out,
|
|
186
|
+
results: annotatedResults,
|
|
379
187
|
staticDiagnosis,
|
|
380
188
|
runSummary: {
|
|
381
|
-
runCompleted: true,
|
|
382
|
-
validatorExitCode,
|
|
383
|
-
validationCoverage: getValidationCoverage({
|
|
384
|
-
results,
|
|
385
|
-
requestedScenario: options.requestedScenario,
|
|
386
|
-
frameworks: selectedFrameworks,
|
|
387
|
-
scenarios: options.scenarios,
|
|
388
|
-
}),
|
|
389
189
|
checkedScenarios: [...options.scenarios],
|
|
190
|
+
cleanup: cleanupOutcome,
|
|
191
|
+
executionStatus,
|
|
390
192
|
omittedScenarios: getSelectableScenarios().filter(scenario => !options.scenarios.has(scenario)),
|
|
391
193
|
requestedScenario: options.requestedScenario,
|
|
194
|
+
runCompleted: true,
|
|
195
|
+
selectedFrameworkIds: selectedFrameworks.map(framework => framework.id),
|
|
196
|
+
validationCoverage: getValidationCoverage(annotatedResults),
|
|
197
|
+
validatorExitCode,
|
|
392
198
|
},
|
|
393
199
|
})
|
|
200
|
+
console.log(`Validation report: ${path.join(out, 'report.md')}`)
|
|
201
|
+
console.log('Present the report and stop. Any correction or retry requires a fresh plan and approval.')
|
|
394
202
|
process.exitCode = validatorExitCode
|
|
395
|
-
} catch (
|
|
396
|
-
process.exitCode =
|
|
397
|
-
|
|
203
|
+
} catch (error) {
|
|
204
|
+
process.exitCode = 3
|
|
205
|
+
if (activeManifest && activeOut) {
|
|
206
|
+
try {
|
|
207
|
+
await writeReport({
|
|
208
|
+
manifest: activeManifest,
|
|
209
|
+
out: activeOut,
|
|
210
|
+
results: [getOrchestrationFailure(error)],
|
|
211
|
+
runSummary: {
|
|
212
|
+
checkedScenarios: [],
|
|
213
|
+
cleanup: cleanupOutcome,
|
|
214
|
+
executionStatus: 'validator_error',
|
|
215
|
+
omittedScenarios: getSelectableScenarios(),
|
|
216
|
+
runCompleted: true,
|
|
217
|
+
selectedFrameworkIds: [],
|
|
218
|
+
validationCoverage: 'partial',
|
|
219
|
+
validatorExitCode: 3,
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
console.error(sanitizeConsoleText(error?.stack || error))
|
|
398
225
|
}
|
|
399
226
|
}
|
|
400
227
|
|
|
401
228
|
/**
|
|
402
|
-
*
|
|
229
|
+
* Runs selected checks for one framework.
|
|
403
230
|
*
|
|
404
|
-
* @param {object}
|
|
405
|
-
* @
|
|
231
|
+
* @param {object} input framework execution inputs
|
|
232
|
+
* @param {object} input.framework framework manifest entry
|
|
233
|
+
* @param {object} input.manifest validation manifest
|
|
234
|
+
* @param {object} input.options validator options
|
|
235
|
+
* @param {string} input.out output directory
|
|
236
|
+
* @param {object[]} input.results accumulated results
|
|
237
|
+
* @param {object} input.staticDiagnosis static diagnosis
|
|
238
|
+
* @returns {Promise<void>} completion
|
|
406
239
|
*/
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
240
|
+
async function validateFramework ({ framework, manifest, options, out, results, staticDiagnosis }) {
|
|
241
|
+
let ciResult
|
|
242
|
+
if (options.scenarios.has(CI_WIRING)) {
|
|
243
|
+
logPhase(framework, 'CI configuration audit', 'start')
|
|
244
|
+
ciResult = runCiWiring({ manifest, framework })
|
|
245
|
+
logPhase(framework, 'CI configuration audit', ciResult.status)
|
|
246
|
+
}
|
|
247
|
+
if (![...options.scenarios].some(scenario => scenario !== CI_WIRING)) {
|
|
248
|
+
if (ciResult) results.push(ciResult)
|
|
249
|
+
return
|
|
410
250
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
251
|
+
|
|
252
|
+
if (framework.status !== 'runnable') {
|
|
253
|
+
results.push(getFrameworkStatusResult(framework))
|
|
254
|
+
if (ciResult) results.push(ciResult)
|
|
255
|
+
addNotReachedLocalResults(results, framework, options.scenarios, 'framework-not-runnable')
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const unavailable = getUnavailableExecutable(getBasicCommand(framework))
|
|
260
|
+
if (unavailable) {
|
|
261
|
+
results.push(getUnavailableRunnerResult(framework, unavailable))
|
|
262
|
+
if (ciResult) results.push(ciResult)
|
|
263
|
+
addNotReachedLocalResults(results, framework, options.scenarios, 'runner-unavailable')
|
|
264
|
+
return
|
|
415
265
|
}
|
|
416
266
|
|
|
267
|
+
const blocker = getStaticBlocker(framework, staticDiagnosis.report)
|
|
268
|
+
if (blocker) {
|
|
269
|
+
const staticFailure = getStaticFailure(framework, blocker, staticDiagnosis.reportPath)
|
|
270
|
+
const basicNotReached = getBasicNotReached(framework, blocker.reason, 'static-project-blocker')
|
|
271
|
+
results.push(staticFailure, basicNotReached)
|
|
272
|
+
if (ciResult) results.push(ciResult)
|
|
273
|
+
addAdvancedNotReached(results, framework, options.scenarios, basicNotReached)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let basicResult
|
|
278
|
+
if (options.scenarios.has(BASIC_REPORTING)) {
|
|
279
|
+
logPhase(framework, 'clean direct test', 'start')
|
|
280
|
+
const preflight = await runFrameworkPreflight({ framework, out, options })
|
|
281
|
+
logPhase(framework, 'clean direct test', preflight.ok ? 'pass' : 'incomplete')
|
|
282
|
+
if (preflight.ok) {
|
|
283
|
+
logPhase(framework, 'Basic Reporting', 'start')
|
|
284
|
+
basicResult = await runBasicReporting({ manifest, framework, out, options })
|
|
285
|
+
logPhase(framework, 'Basic Reporting', basicResult.status)
|
|
286
|
+
} else {
|
|
287
|
+
basicResult = preflight.failure
|
|
288
|
+
}
|
|
289
|
+
results.push(basicResult)
|
|
290
|
+
}
|
|
291
|
+
if (ciResult) results.push(ciResult)
|
|
292
|
+
|
|
293
|
+
const advanced = getAdvancedScenarios(options.scenarios)
|
|
294
|
+
if (advanced.length === 0) return
|
|
295
|
+
if (basicResult?.evidence?.foundationalReportingEstablished !== true) {
|
|
296
|
+
addAdvancedNotReached(results, framework, options.scenarios, basicResult)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const generated = await verifyGeneratedTestStrategy({ framework, out, options })
|
|
301
|
+
if (!generated.ok) {
|
|
302
|
+
results.push(generated.failure)
|
|
303
|
+
addAdvancedNotReached(results, framework, options.scenarios, generated.failure)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
for (const scenario of advanced) {
|
|
307
|
+
// Advanced scenarios are serial because their offline fixtures and generated retry state are isolated.
|
|
308
|
+
// eslint-disable-next-line no-await-in-loop
|
|
309
|
+
results.push(await SCENARIOS[scenario]({ manifest, framework, out, options }))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Creates a manifest scaffold and prints the bounded next step.
|
|
315
|
+
*
|
|
316
|
+
* @param {object} options CLI options
|
|
317
|
+
* @returns {void}
|
|
318
|
+
*/
|
|
319
|
+
function initializeManifest (options) {
|
|
320
|
+
const manifestPath = path.resolve(options.manifest)
|
|
321
|
+
if (path.dirname(manifestPath) !== process.cwd()) {
|
|
322
|
+
throw new Error('The generated manifest must be stored directly in the current repository root.')
|
|
323
|
+
}
|
|
324
|
+
const manifest = createManifestScaffold({ root: process.cwd(), frameworks: options.frameworks })
|
|
325
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
|
|
326
|
+
const targets = manifest.ciDiscovery.reviewTargets
|
|
327
|
+
console.log(sanitizeConsoleText([
|
|
328
|
+
`Created a data-only validation manifest: ${manifestPath}`,
|
|
329
|
+
'No project code ran. The validator selected repository-contained runners and one test file per framework.',
|
|
330
|
+
targets.length > 0
|
|
331
|
+
? `Review only these CI files, in order: ${targets.join(', ')}. Record one exact test job, command, and ` +
|
|
332
|
+
'effective initialization/transport evidence. Set reviewComplete=true only after resolving wrappers and ' +
|
|
333
|
+
'inherited configuration; otherwise leave the uncertainty in unresolved.'
|
|
334
|
+
: 'No supported CI configuration file was found. Leave CI review incomplete.',
|
|
335
|
+
'Do not add commands, setup steps, package scripts, wrapper chains, or fallback tests to the manifest.',
|
|
336
|
+
'Then run --validate-manifest and --print-plan.',
|
|
337
|
+
].join('\n')))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Writes and prints the approval plan.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} manifest loaded manifest
|
|
344
|
+
* @param {object} options CLI options
|
|
345
|
+
* @returns {void}
|
|
346
|
+
*/
|
|
347
|
+
function printPlan (manifest, options) {
|
|
348
|
+
const out = validateOutputPath(manifest, options.out)
|
|
349
|
+
const approvalManifest = getApprovalManifest(manifest, options.frameworks)
|
|
350
|
+
const { plan } = formatExecutionPlanArtifacts({
|
|
351
|
+
manifest: approvalManifest,
|
|
352
|
+
out,
|
|
353
|
+
selectedFrameworkIds: options.frameworks.size > 0
|
|
354
|
+
? approvalManifest.frameworks.map(framework => framework.id)
|
|
355
|
+
: [],
|
|
356
|
+
requestedScenario: options.requestedScenario,
|
|
357
|
+
keepTempFiles: options.keepTempFiles,
|
|
358
|
+
verbose: options.verbose,
|
|
359
|
+
})
|
|
360
|
+
console.log(sanitizeConsoleText([
|
|
361
|
+
'===== CUSTOMER APPROVAL PLAN =====',
|
|
362
|
+
plan,
|
|
363
|
+
'===== END CUSTOMER APPROVAL PLAN =====',
|
|
364
|
+
'',
|
|
365
|
+
`Saved execution plan: ${getExecutionPlanPath(out)}`,
|
|
366
|
+
'LIVE VALIDATION HAS NOT RUN.',
|
|
367
|
+
'Present the complete delimited plan and ask exactly: Approve executing exactly the plan above?',
|
|
368
|
+
].join('\n')))
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Restores approved execution options from approval.json.
|
|
373
|
+
*
|
|
374
|
+
* @param {object} options parsed options
|
|
375
|
+
* @returns {void}
|
|
376
|
+
*/
|
|
377
|
+
function applyApprovedPlanOptions (options) {
|
|
378
|
+
if (!options.approvedArtifactSha256) throw new Error('--run-approved-plan requires --sha256.')
|
|
379
|
+
if (options.approvalOverrides.length > 0) {
|
|
380
|
+
throw new Error('--run-approved-plan cannot be combined with manifest, output, or selection flags.')
|
|
381
|
+
}
|
|
417
382
|
const { material } = loadApprovedPlan(options.runApprovedPlan, options.approvedArtifactSha256)
|
|
418
383
|
options.manifest = material.manifest.path
|
|
419
384
|
options.out = material.validation.outputDirectory
|
|
@@ -428,85 +393,94 @@ function applyApprovedPlanOptions (options) {
|
|
|
428
393
|
options.approvedPlanSha256 = options.approvedArtifactSha256
|
|
429
394
|
}
|
|
430
395
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return out
|
|
439
|
-
}
|
|
440
|
-
|
|
396
|
+
/**
|
|
397
|
+
* Filters manifest frameworks.
|
|
398
|
+
*
|
|
399
|
+
* @param {object[]} frameworks framework entries
|
|
400
|
+
* @param {Set<string>} targets selected ids or kinds
|
|
401
|
+
* @returns {object[]} selected entries
|
|
402
|
+
*/
|
|
441
403
|
function filterFrameworks (frameworks, targets) {
|
|
442
404
|
if (targets.size === 0) return frameworks
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return targets.has(framework.id) || targets.has(framework.framework)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
if (selected.length === 0) {
|
|
449
|
-
throw new Error(`No framework entries matched ${formatFrameworkTargets(targets)}. Available entries: ${
|
|
450
|
-
frameworks.map(framework => framework.id).join(', ') || 'none'
|
|
451
|
-
}`)
|
|
452
|
-
}
|
|
453
|
-
|
|
405
|
+
const selected = frameworks.filter(framework => targets.has(framework.id) || targets.has(framework.framework))
|
|
406
|
+
if (selected.length === 0) throw new Error('No framework matched the requested selection.')
|
|
454
407
|
return selected
|
|
455
408
|
}
|
|
456
409
|
|
|
457
410
|
/**
|
|
458
|
-
*
|
|
411
|
+
* Returns a framework-scoped approval manifest.
|
|
459
412
|
*
|
|
460
413
|
* @param {object} manifest loaded manifest
|
|
461
|
-
* @param {Set<string>} targets selected
|
|
414
|
+
* @param {Set<string>} targets selected frameworks
|
|
462
415
|
* @returns {object} approval manifest
|
|
463
416
|
*/
|
|
464
417
|
function getApprovalManifest (manifest, targets) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const approvalManifest = { ...manifest, frameworks }
|
|
418
|
+
if (targets.size === 0) return manifest
|
|
419
|
+
const approvalManifest = { ...manifest, frameworks: filterFrameworks(manifest.frameworks, targets) }
|
|
469
420
|
Object.defineProperty(approvalManifest, '__sourceSha256', {
|
|
470
|
-
configurable: false,
|
|
471
421
|
enumerable: false,
|
|
472
422
|
value: manifest.__sourceSha256,
|
|
473
|
-
writable: false,
|
|
474
423
|
})
|
|
475
424
|
return approvalManifest
|
|
476
425
|
}
|
|
477
426
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Adds explicit not-reached results for advanced scenarios.
|
|
429
|
+
*
|
|
430
|
+
* @param {object[]} results result list
|
|
431
|
+
* @param {object} framework framework entry
|
|
432
|
+
* @param {Set<string>} scenarios selected scenarios
|
|
433
|
+
* @param {object} blocker blocking result
|
|
434
|
+
* @returns {void}
|
|
435
|
+
*/
|
|
436
|
+
function addAdvancedNotReached (results, framework, scenarios, blocker) {
|
|
437
|
+
for (const scenario of getAdvancedScenarios(scenarios)) {
|
|
438
|
+
results.push({
|
|
439
|
+
frameworkId: framework.id,
|
|
440
|
+
scenario,
|
|
441
|
+
status: 'skip',
|
|
442
|
+
diagnosis: 'Not reached because Basic Reporting did not establish the direct reporting path.',
|
|
443
|
+
evidence: {
|
|
444
|
+
blockedBy: blocker?.scenario || 'basic-reporting',
|
|
445
|
+
validationIncomplete: true,
|
|
446
|
+
},
|
|
447
|
+
artifacts: [],
|
|
448
|
+
})
|
|
482
449
|
}
|
|
483
|
-
return normalized
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function formatFrameworkTargets (targets) {
|
|
487
|
-
return [...targets].map(target => `"${target}"`).join(', ')
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function normalizeScenarioSelection (scenario) {
|
|
491
|
-
if (scenario === BASIC_REPORTING_SCENARIO) return new Set([scenario])
|
|
492
|
-
return new Set([BASIC_REPORTING_SCENARIO, scenario])
|
|
493
450
|
}
|
|
494
451
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Adds local not-reached results for a non-runnable framework.
|
|
454
|
+
*
|
|
455
|
+
* @param {object[]} results result list
|
|
456
|
+
* @param {object} framework framework entry
|
|
457
|
+
* @param {Set<string>} scenarios selected scenarios
|
|
458
|
+
* @param {string} reasonCode blocker id
|
|
459
|
+
* @returns {void}
|
|
460
|
+
*/
|
|
461
|
+
function addNotReachedLocalResults (results, framework, scenarios, reasonCode) {
|
|
462
|
+
for (const scenario of scenarios) {
|
|
463
|
+
if (scenario === CI_WIRING) continue
|
|
464
|
+
results.push({
|
|
465
|
+
frameworkId: framework.id,
|
|
466
|
+
scenario,
|
|
467
|
+
status: 'skip',
|
|
468
|
+
diagnosis: 'Not reached because this framework has no available direct-runner validation target.',
|
|
469
|
+
evidence: { reasonCode, validationIncomplete: true },
|
|
470
|
+
artifacts: [],
|
|
471
|
+
})
|
|
472
|
+
}
|
|
499
473
|
}
|
|
500
474
|
|
|
501
475
|
/**
|
|
502
|
-
*
|
|
476
|
+
* Adds fail-closed results for missing orchestration output.
|
|
503
477
|
*
|
|
504
|
-
* @param {object[]} results
|
|
505
|
-
* @param {object[]} frameworks
|
|
478
|
+
* @param {object[]} results result list
|
|
479
|
+
* @param {object[]} frameworks selected frameworks
|
|
506
480
|
* @param {Set<string>} scenarios selected scenarios
|
|
507
481
|
* @returns {void}
|
|
508
482
|
*/
|
|
509
|
-
function
|
|
483
|
+
function addMissingResults (results, frameworks, scenarios) {
|
|
510
484
|
for (const framework of frameworks) {
|
|
511
485
|
for (const scenario of scenarios) {
|
|
512
486
|
if (results.some(result => result.frameworkId === framework.id && result.scenario === scenario)) continue
|
|
@@ -514,12 +488,8 @@ function addMissingRequiredResults (results, frameworks, scenarios) {
|
|
|
514
488
|
frameworkId: framework.id,
|
|
515
489
|
scenario,
|
|
516
490
|
status: 'error',
|
|
517
|
-
diagnosis:
|
|
518
|
-
evidence: {
|
|
519
|
-
validationIncomplete: true,
|
|
520
|
-
recommendation: 'Rerun the validator. If the check remains absent, report this validator orchestration ' +
|
|
521
|
-
'error instead of treating the validation as successful.',
|
|
522
|
-
},
|
|
491
|
+
diagnosis: 'The selected check produced no result. This is a validator orchestration error.',
|
|
492
|
+
evidence: { validationIncomplete: true, validationOrchestrationFailed: true },
|
|
523
493
|
artifacts: [],
|
|
524
494
|
})
|
|
525
495
|
}
|
|
@@ -527,377 +497,242 @@ function addMissingRequiredResults (results, frameworks, scenarios) {
|
|
|
527
497
|
}
|
|
528
498
|
|
|
529
499
|
/**
|
|
530
|
-
*
|
|
500
|
+
* Returns selected advanced scenario ids.
|
|
531
501
|
*
|
|
532
|
-
* @param {
|
|
533
|
-
* @
|
|
534
|
-
* @param {string|null} input.requestedScenario explicitly selected scenario
|
|
535
|
-
* @param {object[]} input.frameworks selected manifest frameworks
|
|
536
|
-
* @param {Set<string>} input.scenarios selected scenarios
|
|
537
|
-
* @returns {'complete'|'partial'} validation coverage
|
|
502
|
+
* @param {Set<string>} scenarios selected scenarios
|
|
503
|
+
* @returns {string[]} advanced scenarios
|
|
538
504
|
*/
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
if (results.some(result => result.evidence?.manifestIncomplete || result.evidence?.validationIncomplete)) {
|
|
542
|
-
return 'partial'
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const runnableFrameworks = frameworks.filter(framework => framework.status === 'runnable')
|
|
546
|
-
if (runnableFrameworks.length === 0) return 'partial'
|
|
547
|
-
for (const framework of runnableFrameworks) {
|
|
548
|
-
for (const scenario of scenarios) {
|
|
549
|
-
if (!results.some(result => result.frameworkId === framework.id && result.scenario === scenario)) {
|
|
550
|
-
return 'partial'
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return 'complete'
|
|
505
|
+
function getAdvancedScenarios (scenarios) {
|
|
506
|
+
return Object.keys(SCENARIOS).filter(scenario => scenario !== BASIC_REPORTING && scenarios.has(scenario))
|
|
555
507
|
}
|
|
556
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Returns all selectable scenarios.
|
|
511
|
+
*
|
|
512
|
+
* @returns {string[]} scenario ids
|
|
513
|
+
*/
|
|
557
514
|
function getSelectableScenarios () {
|
|
558
|
-
return [
|
|
559
|
-
BASIC_REPORTING_SCENARIO,
|
|
560
|
-
CI_WIRING_SCENARIO,
|
|
561
|
-
...Object.keys(SCENARIOS).filter(scenario => scenario !== BASIC_REPORTING_SCENARIO),
|
|
562
|
-
]
|
|
515
|
+
return [...Object.keys(SCENARIOS), CI_WIRING]
|
|
563
516
|
}
|
|
564
517
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
basicReportingStatus: basicResult.status,
|
|
576
|
-
basicReportingDiagnosis: basicResult.diagnosis,
|
|
577
|
-
featureEligibility: {
|
|
578
|
-
eligible: false,
|
|
579
|
-
blockedBy: BASIC_REPORTING_SCENARIO,
|
|
580
|
-
reasonCode: 'basic-reporting-failed',
|
|
581
|
-
scenario: 'ci-wiring',
|
|
582
|
-
},
|
|
583
|
-
ciWiring: framework.ciWiring,
|
|
584
|
-
},
|
|
585
|
-
artifacts: [],
|
|
586
|
-
}
|
|
518
|
+
/**
|
|
519
|
+
* Normalizes scenario selection.
|
|
520
|
+
*
|
|
521
|
+
* @param {string} scenario requested scenario
|
|
522
|
+
* @returns {Set<string>} effective scenarios
|
|
523
|
+
*/
|
|
524
|
+
function normalizeScenarioSelection (scenario) {
|
|
525
|
+
if (!getSelectableScenarios().includes(scenario)) throw new Error(`Unknown scenario: ${scenario}`)
|
|
526
|
+
if (scenario === BASIC_REPORTING || scenario === CI_WIRING) return new Set([scenario])
|
|
527
|
+
return new Set([BASIC_REPORTING, scenario])
|
|
587
528
|
}
|
|
588
529
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
featureEligibility: {
|
|
600
|
-
eligible: false,
|
|
601
|
-
blockedBy: BASIC_REPORTING_SCENARIO,
|
|
602
|
-
reasonCode: 'basic-reporting-failed',
|
|
603
|
-
scenario,
|
|
604
|
-
},
|
|
605
|
-
},
|
|
606
|
-
artifacts: [],
|
|
607
|
-
}
|
|
530
|
+
/**
|
|
531
|
+
* Normalizes a framework target.
|
|
532
|
+
*
|
|
533
|
+
* @param {string} target target value
|
|
534
|
+
* @returns {string} normalized target
|
|
535
|
+
*/
|
|
536
|
+
function normalizeFrameworkTarget (target) {
|
|
537
|
+
const normalized = String(target).trim().replaceAll(/:+$/g, '')
|
|
538
|
+
if (!normalized) throw new Error('Framework target cannot be empty.')
|
|
539
|
+
return normalized
|
|
608
540
|
}
|
|
609
541
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
reasonCode: 'generated-test-verification-failed',
|
|
624
|
-
scenario,
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
artifacts: [],
|
|
542
|
+
/**
|
|
543
|
+
* Validates the output directory.
|
|
544
|
+
*
|
|
545
|
+
* @param {object} manifest loaded manifest
|
|
546
|
+
* @param {string} outputPath output path
|
|
547
|
+
* @returns {string} absolute output path
|
|
548
|
+
*/
|
|
549
|
+
function validateOutputPath (manifest, outputPath) {
|
|
550
|
+
const root = path.resolve(manifest.repository.root)
|
|
551
|
+
const out = path.resolve(outputPath)
|
|
552
|
+
const relative = path.relative(root, out)
|
|
553
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
554
|
+
throw new Error('Validation output directory must be a dedicated child of repository.root.')
|
|
628
555
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
function isUnsuccessfulResult (result) {
|
|
632
|
-
return result.status === 'fail' || result.status === 'error' || result.status === 'blocked'
|
|
556
|
+
return out
|
|
633
557
|
}
|
|
634
558
|
|
|
635
559
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* Framework discovery-only entries use the synthetic `all` scenario and do not prove Test Optimization behavior.
|
|
560
|
+
* Prevents incompatible CLI modes.
|
|
639
561
|
*
|
|
640
|
-
* @param {object
|
|
641
|
-
* @returns {
|
|
562
|
+
* @param {object} options parsed options
|
|
563
|
+
* @returns {void}
|
|
642
564
|
*/
|
|
643
|
-
function
|
|
644
|
-
|
|
565
|
+
function assertCompatibleModes (options) {
|
|
566
|
+
if (!options.runApprovedPlan) return
|
|
567
|
+
if (options.help || options.initManifest || options.printPlan || options.validateManifest) {
|
|
568
|
+
throw new Error('--run-approved-plan cannot be combined with another mode.')
|
|
569
|
+
}
|
|
645
570
|
}
|
|
646
571
|
|
|
647
572
|
/**
|
|
648
|
-
*
|
|
573
|
+
* Reads a required flag value.
|
|
649
574
|
*
|
|
650
|
-
* @param {
|
|
651
|
-
* @param {
|
|
652
|
-
* @
|
|
575
|
+
* @param {string[]} argv arguments
|
|
576
|
+
* @param {number} index value index
|
|
577
|
+
* @param {string} flag flag name
|
|
578
|
+
* @returns {string} flag value
|
|
653
579
|
*/
|
|
654
|
-
function
|
|
655
|
-
|
|
580
|
+
function requireValue (argv, index, flag) {
|
|
581
|
+
if (!argv[index]) throw new Error(`${flag} requires a value.`)
|
|
582
|
+
return argv[index]
|
|
656
583
|
}
|
|
657
584
|
|
|
658
585
|
/**
|
|
659
|
-
* Prints
|
|
586
|
+
* Prints CLI help.
|
|
660
587
|
*
|
|
661
|
-
* @param {object} framework manifest framework entry
|
|
662
|
-
* @param {string} phase customer-facing phase name
|
|
663
|
-
* @param {string|undefined} status phase outcome
|
|
664
588
|
* @returns {void}
|
|
665
589
|
*/
|
|
666
|
-
function
|
|
667
|
-
|
|
590
|
+
function printHelp () {
|
|
591
|
+
console.log(`Usage: node ci/validate-test-optimization.js [options]
|
|
592
|
+
|
|
593
|
+
--init-manifest
|
|
594
|
+
--validate-manifest
|
|
595
|
+
--print-plan
|
|
596
|
+
--run-approved-plan <approval.json> --sha256 <digest>
|
|
597
|
+
--manifest <path>
|
|
598
|
+
--out <path>
|
|
599
|
+
--framework <id>
|
|
600
|
+
--scenario <${getSelectableScenarios().join('|')}>
|
|
601
|
+
--keep-temp-files
|
|
602
|
+
--verbose`)
|
|
668
603
|
}
|
|
669
604
|
|
|
670
605
|
/**
|
|
671
|
-
*
|
|
606
|
+
* Logs one phase transition.
|
|
672
607
|
*
|
|
673
|
-
* @param {
|
|
608
|
+
* @param {object} framework framework entry
|
|
609
|
+
* @param {string} phase phase name
|
|
610
|
+
* @param {string} status status
|
|
674
611
|
* @returns {void}
|
|
675
612
|
*/
|
|
676
|
-
function
|
|
677
|
-
console.log(sanitizeConsoleText(`[test-optimization-validator] ${
|
|
613
|
+
function logPhase (framework, phase, status) {
|
|
614
|
+
console.log(sanitizeConsoleText(`[test-optimization-validator] ${framework.id}: ${phase}: ${status}`))
|
|
678
615
|
}
|
|
679
616
|
|
|
680
617
|
/**
|
|
681
|
-
*
|
|
618
|
+
* Builds a non-runnable framework result.
|
|
682
619
|
*
|
|
683
|
-
* @param {
|
|
684
|
-
* @returns {
|
|
620
|
+
* @param {object} framework framework entry
|
|
621
|
+
* @returns {object} result
|
|
685
622
|
*/
|
|
686
|
-
function
|
|
687
|
-
|
|
688
|
-
'basic-reporting': 'Basic Reporting',
|
|
689
|
-
'ci-wiring': 'CI Wiring',
|
|
690
|
-
efd: 'Early Flake Detection',
|
|
691
|
-
atr: 'Auto Test Retries',
|
|
692
|
-
'test-management': 'Test Management',
|
|
693
|
-
}[scenario] || scenario
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function getStaticFailure (framework, blocker, staticDiagnosisPath) {
|
|
623
|
+
function getFrameworkStatusResult (framework) {
|
|
624
|
+
const requiresProjectSetup = framework.status === 'requires_manual_setup'
|
|
697
625
|
return {
|
|
698
626
|
frameworkId: framework.id,
|
|
699
627
|
scenario: 'all',
|
|
700
|
-
status: '
|
|
701
|
-
diagnosis:
|
|
628
|
+
status: 'skip',
|
|
629
|
+
diagnosis: framework.notes?.[0] || `Framework status is ${framework.status}.`,
|
|
702
630
|
evidence: {
|
|
703
|
-
|
|
704
|
-
|
|
631
|
+
frameworkStatus: framework.status,
|
|
632
|
+
validationIncomplete: true,
|
|
633
|
+
...(requiresProjectSetup
|
|
634
|
+
? {
|
|
635
|
+
blockedByProjectSetup: true,
|
|
636
|
+
...(framework.notes?.[0] ? { recommendation: framework.notes[0] } : {}),
|
|
637
|
+
}
|
|
638
|
+
: { validatorAdapterUnavailable: true }),
|
|
705
639
|
},
|
|
706
|
-
artifacts: [
|
|
640
|
+
artifacts: [],
|
|
707
641
|
}
|
|
708
642
|
}
|
|
709
643
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
scenario: 'all',
|
|
719
|
-
status: 'skip',
|
|
720
|
-
diagnosis: `${frameworkName} is not supported as a Test Optimization test framework.`,
|
|
721
|
-
evidence: {
|
|
722
|
-
...evidence,
|
|
723
|
-
recommendation: 'Choose a supported framework before running live validation.',
|
|
724
|
-
},
|
|
725
|
-
artifacts: [],
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Builds an unavailable runner result.
|
|
646
|
+
*
|
|
647
|
+
* @param {object} framework framework entry
|
|
648
|
+
* @param {string} unavailable missing file
|
|
649
|
+
* @returns {object} result
|
|
650
|
+
*/
|
|
651
|
+
function getUnavailableRunnerResult (framework, unavailable) {
|
|
729
652
|
return {
|
|
730
653
|
frameworkId: framework.id,
|
|
731
654
|
scenario: 'all',
|
|
732
655
|
status: 'skip',
|
|
733
|
-
diagnosis:
|
|
734
|
-
evidence: {
|
|
735
|
-
...evidence,
|
|
736
|
-
recommendation: 'Provide a small runnable command for this framework, or mark the setup blocker explicitly.',
|
|
737
|
-
},
|
|
656
|
+
diagnosis: `The direct runner is unavailable: ${unavailable}. Complete normal project setup and retry.`,
|
|
657
|
+
evidence: { blockedByProjectSetup: true, unavailableRunner: unavailable, validationIncomplete: true },
|
|
738
658
|
artifacts: [],
|
|
739
659
|
}
|
|
740
660
|
}
|
|
741
661
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
return `${frameworkName} was detected, but no runnable validation command was available. ` +
|
|
752
|
-
`Basic reporting was not run. Manifest reason: ${notes[0]}`
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return `${frameworkName} was detected, but the manifest did not prove a runnable validation command. ` +
|
|
756
|
-
'Basic reporting was not run. See discovery evidence for scripts/config files to turn into a small command.'
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function isDependencyOnlyDetection (evidence) {
|
|
760
|
-
return evidence.directDependency && evidence.configFiles.length === 0 && evidence.frameworkScripts.length === 0
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function getDependencyOnlyDiagnosis (framework, evidence) {
|
|
764
|
-
const frameworkName = getDisplayFrameworkName(framework.framework)
|
|
765
|
-
const dependency = formatDependency(evidence.directDependency)
|
|
766
|
-
const note = evidence.manifestNotes?.[0] ? ` Manifest note: ${evidence.manifestNotes[0]}` : ''
|
|
767
|
-
const common = `${frameworkName} is installed${dependency}, but this repository does not appear to use ` +
|
|
768
|
-
`${frameworkName} to run tests: no ${framework.framework} config, package script, or runnable ` +
|
|
769
|
-
`${framework.framework} test command was found. Basic reporting was not run for ${frameworkName}.`
|
|
770
|
-
|
|
771
|
-
if (framework.framework === 'playwright') {
|
|
772
|
-
return `${frameworkName} is installed${dependency}, but no Playwright Test setup was found. ` +
|
|
773
|
-
'The playwright package can be used only for browser automation; Test Optimization validation needs a ' +
|
|
774
|
-
'`playwright test` setup with a config, script, or runnable test command. Basic reporting was not run ' +
|
|
775
|
-
`for ${frameworkName}.${note}`
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
return `${common} If this repo does use ${frameworkName}, provide a small ${frameworkName} test command; ` +
|
|
779
|
-
`otherwise this dependency-only detection can be ignored.${note}`
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function getDisplayFrameworkName (frameworkName) {
|
|
783
|
-
return {
|
|
784
|
-
cucumber: 'Cucumber',
|
|
785
|
-
cypress: 'Cypress',
|
|
786
|
-
jest: 'Jest',
|
|
787
|
-
mocha: 'Mocha',
|
|
788
|
-
playwright: 'Playwright',
|
|
789
|
-
vitest: 'Vitest',
|
|
790
|
-
}[frameworkName] || frameworkName
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function formatDependency (dependency) {
|
|
794
|
-
if (!dependency) return ''
|
|
795
|
-
return ` in ${dependency.field}${dependency.version ? ` (${dependency.version})` : ''}`
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function getFrameworkStatusEvidence (framework) {
|
|
799
|
-
const root = framework.project?.root
|
|
662
|
+
/**
|
|
663
|
+
* Builds a static project blocker result.
|
|
664
|
+
*
|
|
665
|
+
* @param {object} framework framework entry
|
|
666
|
+
* @param {object} blocker static blocker
|
|
667
|
+
* @param {string} reportPath static report path
|
|
668
|
+
* @returns {object} result
|
|
669
|
+
*/
|
|
670
|
+
function getStaticFailure (framework, blocker, reportPath) {
|
|
800
671
|
return {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
configFiles: root ? findFrameworkConfigFiles(root, framework.framework) : [],
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function getDirectDependency (root, frameworkName) {
|
|
812
|
-
const packageJson = readPackageJson(root)
|
|
813
|
-
if (!packageJson) return
|
|
814
|
-
|
|
815
|
-
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
816
|
-
const value = packageJson[field]?.[frameworkName]
|
|
817
|
-
if (value) return { field, version: value }
|
|
672
|
+
frameworkId: framework.id,
|
|
673
|
+
scenario: 'all',
|
|
674
|
+
status: 'error',
|
|
675
|
+
diagnosis: blocker.reason,
|
|
676
|
+
evidence: { blockedByProjectSetup: true, staticDiagnosis: reportPath, validationIncomplete: true },
|
|
677
|
+
artifacts: [reportPath],
|
|
818
678
|
}
|
|
819
679
|
}
|
|
820
680
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Builds a Basic Reporting not-reached result.
|
|
683
|
+
*
|
|
684
|
+
* @param {object} framework framework entry
|
|
685
|
+
* @param {string} diagnosis blocker diagnosis
|
|
686
|
+
* @param {string} reasonCode blocker id
|
|
687
|
+
* @returns {object} result
|
|
688
|
+
*/
|
|
689
|
+
function getBasicNotReached (framework, diagnosis, reasonCode) {
|
|
690
|
+
return {
|
|
691
|
+
frameworkId: framework.id,
|
|
692
|
+
scenario: BASIC_REPORTING,
|
|
693
|
+
status: 'skip',
|
|
694
|
+
diagnosis: `Basic Reporting was not reached: ${diagnosis}`,
|
|
695
|
+
evidence: { reasonCode, validationIncomplete: true },
|
|
696
|
+
artifacts: [],
|
|
837
697
|
}
|
|
838
|
-
return matches.slice(0, 8)
|
|
839
698
|
}
|
|
840
699
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
function readPackageJson (root) {
|
|
861
|
-
try {
|
|
862
|
-
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
863
|
-
} catch {
|
|
864
|
-
return null
|
|
700
|
+
/**
|
|
701
|
+
* Builds a cleanup failure.
|
|
702
|
+
*
|
|
703
|
+
* @param {Error|undefined} error cleanup error
|
|
704
|
+
* @param {object} cleanup cleanup outcome
|
|
705
|
+
* @returns {object} result
|
|
706
|
+
*/
|
|
707
|
+
function getCleanupFailure (error, cleanup) {
|
|
708
|
+
const retained = (cleanup.filesRetained || 0) + (cleanup.directoriesRetained || 0)
|
|
709
|
+
const reason = error?.message || error ||
|
|
710
|
+
`${retained} temporary validation path${retained === 1 ? '' : 's'} remained after safe cleanup`
|
|
711
|
+
return {
|
|
712
|
+
frameworkId: 'validation-cleanup',
|
|
713
|
+
scenario: 'all',
|
|
714
|
+
status: 'error',
|
|
715
|
+
diagnosis: `Temporary validation files could not be removed safely: ${reason}`,
|
|
716
|
+
evidence: { cleanup, cleanupFailed: true, validationIncomplete: true },
|
|
717
|
+
artifacts: [],
|
|
865
718
|
}
|
|
866
719
|
}
|
|
867
720
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (!findFiles(filename, depth - 1, visit)) return false
|
|
883
|
-
} else if (!visit(filename)) {
|
|
884
|
-
return false
|
|
885
|
-
}
|
|
721
|
+
/**
|
|
722
|
+
* Builds a top-level orchestration failure.
|
|
723
|
+
*
|
|
724
|
+
* @param {Error} error failure
|
|
725
|
+
* @returns {object} result
|
|
726
|
+
*/
|
|
727
|
+
function getOrchestrationFailure (error) {
|
|
728
|
+
return {
|
|
729
|
+
frameworkId: 'validator',
|
|
730
|
+
scenario: 'all',
|
|
731
|
+
status: 'error',
|
|
732
|
+
diagnosis: error?.message || String(error),
|
|
733
|
+
evidence: { validationIncomplete: true, validationOrchestrationFailed: true },
|
|
734
|
+
artifacts: [],
|
|
886
735
|
}
|
|
887
|
-
|
|
888
|
-
return true
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function shouldSkipDirectory (name) {
|
|
892
|
-
return name === '.git' || name === 'node_modules' || name === 'dist' || name === 'coverage'
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function includesWord (value, word) {
|
|
896
|
-
return new RegExp(`(^|[^a-zA-Z0-9_-])${escapeRegExp(word)}([^a-zA-Z0-9_-]|$)`).test(value)
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function escapeRegExp (value) {
|
|
900
|
-
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
|
|
901
736
|
}
|
|
902
737
|
|
|
903
738
|
module.exports = { filterFrameworks, main, normalizeFrameworkTarget, parseArgs }
|