dd-trace 6.4.0 → 6.6.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/README.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +11 -8
- package/packages/datadog-instrumentations/src/ai.js +8 -2
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +31 -19
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/jest.js +44 -17
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +162 -46
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +18 -8
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
- package/packages/datadog-plugin-cucumber/src/index.js +3 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +87 -45
- package/packages/datadog-plugin-graphql/src/validate.js +2 -2
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-mocha/src/index.js +2 -1
- package/packages/datadog-plugin-playwright/src/index.js +181 -65
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-vitest/src/index.js +5 -5
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +66 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +10 -3
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +68 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +16 -27
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +56 -4
- 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/proxy.js +84 -7
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -14
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
|
|
8
|
+
const { getFrameworkDefinitions } = require('../diagnose')
|
|
9
|
+
const { DD_MAJOR } = require('../../version')
|
|
10
|
+
|
|
11
|
+
const { assertApprovalDigest, getApprovalDigest } = require('./approval')
|
|
12
|
+
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')
|
|
19
|
+
const { cleanupGeneratedFiles } = require('./generated-files')
|
|
20
|
+
const { verifyGeneratedTestStrategy } = require('./generated-verifier')
|
|
21
|
+
const { annotateCiDiscovery } = require('./ci-discovery')
|
|
22
|
+
const { loadManifest } = require('./manifest-loader')
|
|
23
|
+
const { createManifestScaffold } = require('./manifest-scaffold')
|
|
24
|
+
const { formatExecutionPlan, getApprovalSummaryPath, getExecutionPlanPath } = require('./plan-writer')
|
|
25
|
+
const { runFrameworkPreflight } = require('./preflight-runner')
|
|
26
|
+
const { sanitizeConsoleText } = require('./redaction')
|
|
27
|
+
const { writePendingReport, writeReport } = require('./report-writer')
|
|
28
|
+
const { ensureSafeDirectory } = require('./safe-files')
|
|
29
|
+
const { runSetupCommands } = require('./setup-runner')
|
|
30
|
+
const {
|
|
31
|
+
getStaticBlocker,
|
|
32
|
+
runStaticDiagnosis,
|
|
33
|
+
} = require('./static-diagnosis')
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MANIFEST = './dd-test-optimization-validation-manifest.json'
|
|
36
|
+
const DEFAULT_OUT = './dd-test-optimization-validation-results'
|
|
37
|
+
|
|
38
|
+
const SCENARIOS = {
|
|
39
|
+
'basic-reporting': runBasicReporting,
|
|
40
|
+
efd: runEarlyFlakeDetection,
|
|
41
|
+
atr: runAutoTestRetries,
|
|
42
|
+
'test-management': runTestManagement,
|
|
43
|
+
}
|
|
44
|
+
const BASIC_REPORTING_SCENARIO = 'basic-reporting'
|
|
45
|
+
const CI_WIRING_SCENARIO = 'ci-wiring'
|
|
46
|
+
|
|
47
|
+
function parseArgs (argv) {
|
|
48
|
+
const options = {
|
|
49
|
+
manifest: DEFAULT_MANIFEST,
|
|
50
|
+
out: DEFAULT_OUT,
|
|
51
|
+
frameworks: new Set(),
|
|
52
|
+
scenarios: new Set(getSelectableScenarios()),
|
|
53
|
+
requestedScenario: null,
|
|
54
|
+
keepTempFiles: false,
|
|
55
|
+
verbose: false,
|
|
56
|
+
approvalOverrides: [],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const arg = argv[i]
|
|
61
|
+
switch (arg) {
|
|
62
|
+
case '--manifest':
|
|
63
|
+
options.manifest = requireValue(argv, ++i, arg)
|
|
64
|
+
options.approvalOverrides.push(arg)
|
|
65
|
+
break
|
|
66
|
+
case '--out':
|
|
67
|
+
options.out = requireValue(argv, ++i, arg)
|
|
68
|
+
options.approvalOverrides.push(arg)
|
|
69
|
+
break
|
|
70
|
+
case '--framework':
|
|
71
|
+
options.frameworks.add(normalizeFrameworkTarget(requireValue(argv, ++i, arg)))
|
|
72
|
+
options.approvalOverrides.push(arg)
|
|
73
|
+
break
|
|
74
|
+
case '--scenario':
|
|
75
|
+
options.requestedScenario = requireValue(argv, ++i, arg)
|
|
76
|
+
options.scenarios = normalizeScenarioSelection(options.requestedScenario)
|
|
77
|
+
options.approvalOverrides.push(arg)
|
|
78
|
+
break
|
|
79
|
+
case '--keep-temp-files':
|
|
80
|
+
options.keepTempFiles = true
|
|
81
|
+
options.approvalOverrides.push(arg)
|
|
82
|
+
break
|
|
83
|
+
case '--verbose':
|
|
84
|
+
options.verbose = true
|
|
85
|
+
options.approvalOverrides.push(arg)
|
|
86
|
+
break
|
|
87
|
+
case '--validate-manifest':
|
|
88
|
+
options.validateManifest = true
|
|
89
|
+
break
|
|
90
|
+
case '--init-manifest':
|
|
91
|
+
options.initManifest = true
|
|
92
|
+
break
|
|
93
|
+
case '--print-plan':
|
|
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}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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
|
+
return options
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requireValue (argv, index, flag) {
|
|
130
|
+
if (!argv[index]) {
|
|
131
|
+
throw new Error(`${flag} requires a value`)
|
|
132
|
+
}
|
|
133
|
+
return argv[index]
|
|
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
|
+
|
|
157
|
+
async function main (argv) {
|
|
158
|
+
try {
|
|
159
|
+
const options = parseArgs(argv)
|
|
160
|
+
if (options.help) {
|
|
161
|
+
printHelp()
|
|
162
|
+
return
|
|
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
|
+
}
|
|
180
|
+
|
|
181
|
+
if (options.runApprovedPlan) applyApprovedPlanOptions(options)
|
|
182
|
+
|
|
183
|
+
const manifest = loadManifest(options.manifest)
|
|
184
|
+
if (options.printPlan) {
|
|
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
|
+
}
|
|
224
|
+
if (options.validateManifest) {
|
|
225
|
+
console.log(sanitizeConsoleText(`Validation manifest is valid: ${manifest.__path}`))
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
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
|
+
)
|
|
233
|
+
}
|
|
234
|
+
const out = validateOutputPath(manifest, options.out)
|
|
235
|
+
options.repositoryRoot = manifest.repository.root
|
|
236
|
+
const selectedFrameworks = filterFrameworks(manifest.frameworks, options.frameworks)
|
|
237
|
+
const approvalManifest = getApprovalManifest(manifest, options.frameworks)
|
|
238
|
+
assertApprovalDigest(options.approvedPlanSha256, {
|
|
239
|
+
manifest: approvalManifest,
|
|
240
|
+
out,
|
|
241
|
+
selectedFrameworkIds: options.frameworks.size > 0
|
|
242
|
+
? selectedFrameworks.map(framework => framework.id)
|
|
243
|
+
: [],
|
|
244
|
+
requestedScenario: options.requestedScenario,
|
|
245
|
+
offlineFixtureNonce: options.offlineFixtureNonce,
|
|
246
|
+
keepTempFiles: options.keepTempFiles,
|
|
247
|
+
verbose: options.verbose,
|
|
248
|
+
})
|
|
249
|
+
options.requireExecutableApproval = true
|
|
250
|
+
ensureSafeDirectory(manifest.repository.root, out, 'validation output directory', { allowRootSymlink: true })
|
|
251
|
+
if (writePendingReport) writePendingReport({ manifest, out })
|
|
252
|
+
const staticDiagnosis = runStaticDiagnosis({ manifest, out })
|
|
253
|
+
annotateCiDiscovery({ manifest, diagnosis: staticDiagnosis.report })
|
|
254
|
+
|
|
255
|
+
const results = []
|
|
256
|
+
const runnableFrameworks = []
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const frameworks = filterFrameworks(manifest.frameworks, options.frameworks)
|
|
260
|
+
const liveReadyFrameworks = []
|
|
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')
|
|
280
|
+
// eslint-disable-next-line no-await-in-loop
|
|
281
|
+
const setup = await runSetupCommands({ framework, out, options })
|
|
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)
|
|
291
|
+
}
|
|
292
|
+
for (const framework of runnableFrameworks) {
|
|
293
|
+
let basicResult
|
|
294
|
+
if (options.scenarios.has(BASIC_REPORTING_SCENARIO)) {
|
|
295
|
+
// The validator owns the dd-trace-less control so ambient agent initialization cannot contaminate it.
|
|
296
|
+
logPhaseStart(framework, 'Test execution without Datadog')
|
|
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)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} finally {
|
|
370
|
+
await cleanupGeneratedFiles(manifest, { keep: options.keepTempFiles })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
addMissingRequiredResults(results, runnableFrameworks, options.scenarios)
|
|
374
|
+
const validatorExitCode = results.some(isUnsuccessfulResult) || !didRunLiveValidation(results) ? 1 : 0
|
|
375
|
+
await writeReport({
|
|
376
|
+
manifest,
|
|
377
|
+
results,
|
|
378
|
+
out,
|
|
379
|
+
staticDiagnosis,
|
|
380
|
+
runSummary: {
|
|
381
|
+
runCompleted: true,
|
|
382
|
+
validatorExitCode,
|
|
383
|
+
validationCoverage: getValidationCoverage({
|
|
384
|
+
results,
|
|
385
|
+
requestedScenario: options.requestedScenario,
|
|
386
|
+
frameworks: selectedFrameworks,
|
|
387
|
+
scenarios: options.scenarios,
|
|
388
|
+
}),
|
|
389
|
+
checkedScenarios: [...options.scenarios],
|
|
390
|
+
omittedScenarios: getSelectableScenarios().filter(scenario => !options.scenarios.has(scenario)),
|
|
391
|
+
requestedScenario: options.requestedScenario,
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
process.exitCode = validatorExitCode
|
|
395
|
+
} catch (err) {
|
|
396
|
+
process.exitCode = 1
|
|
397
|
+
console.error(sanitizeConsoleText(err && err.stack ? err.stack : err))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Reconstructs live options from a hash-verified approval artifact.
|
|
403
|
+
*
|
|
404
|
+
* @param {object} options parsed CLI options
|
|
405
|
+
* @returns {void}
|
|
406
|
+
*/
|
|
407
|
+
function applyApprovedPlanOptions (options) {
|
|
408
|
+
if (!options.approvedArtifactSha256) {
|
|
409
|
+
throw new Error('--run-approved-plan requires --sha256 from the reviewed execution plan.')
|
|
410
|
+
}
|
|
411
|
+
if (options.approvalOverrides.length > 0 || options.offlineFixtureNonce || options.approvedPlanSha256) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
'--run-approved-plan cannot be combined with manifest, output, selection, or legacy approval flags.'
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const { material } = loadApprovedPlan(options.runApprovedPlan, options.approvedArtifactSha256)
|
|
418
|
+
options.manifest = material.manifest.path
|
|
419
|
+
options.out = material.validation.outputDirectory
|
|
420
|
+
options.frameworks = new Set(material.selection.frameworks.map(normalizeFrameworkTarget))
|
|
421
|
+
options.requestedScenario = material.selection.scenario
|
|
422
|
+
options.scenarios = options.requestedScenario
|
|
423
|
+
? normalizeScenarioSelection(options.requestedScenario)
|
|
424
|
+
: new Set(getSelectableScenarios())
|
|
425
|
+
options.offlineFixtureNonce = material.validation.offlineFixtureNonce
|
|
426
|
+
options.keepTempFiles = material.validation.keepTemporaryFiles === true
|
|
427
|
+
options.verbose = material.validation.verbose === true
|
|
428
|
+
options.approvedPlanSha256 = options.approvedArtifactSha256
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function validateOutputPath (manifest, outputPath) {
|
|
432
|
+
const root = path.resolve(manifest.repository.root)
|
|
433
|
+
const out = path.resolve(outputPath)
|
|
434
|
+
const relative = path.relative(root, out)
|
|
435
|
+
if (relative === '' || !relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
436
|
+
throw new Error('Validation output directory must be a dedicated child directory inside repository.root.')
|
|
437
|
+
}
|
|
438
|
+
return out
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function filterFrameworks (frameworks, targets) {
|
|
442
|
+
if (targets.size === 0) return frameworks
|
|
443
|
+
|
|
444
|
+
const selected = frameworks.filter(framework => {
|
|
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
|
+
|
|
454
|
+
return selected
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Creates the manifest view covered by a framework-scoped approval.
|
|
459
|
+
*
|
|
460
|
+
* @param {object} manifest loaded manifest
|
|
461
|
+
* @param {Set<string>} targets selected framework targets
|
|
462
|
+
* @returns {object} approval manifest
|
|
463
|
+
*/
|
|
464
|
+
function getApprovalManifest (manifest, targets) {
|
|
465
|
+
const frameworks = filterFrameworks(manifest.frameworks, targets)
|
|
466
|
+
if (frameworks === manifest.frameworks) return manifest
|
|
467
|
+
|
|
468
|
+
const approvalManifest = { ...manifest, frameworks }
|
|
469
|
+
Object.defineProperty(approvalManifest, '__sourceSha256', {
|
|
470
|
+
configurable: false,
|
|
471
|
+
enumerable: false,
|
|
472
|
+
value: manifest.__sourceSha256,
|
|
473
|
+
writable: false,
|
|
474
|
+
})
|
|
475
|
+
return approvalManifest
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function normalizeFrameworkTarget (target) {
|
|
479
|
+
const normalized = String(target).trim().replaceAll(/:+$/g, '')
|
|
480
|
+
if (!normalized) {
|
|
481
|
+
throw new Error('Framework target cannot be empty')
|
|
482
|
+
}
|
|
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
|
+
}
|
|
494
|
+
|
|
495
|
+
function getAdvancedScenarios (scenarios) {
|
|
496
|
+
return Object.keys(SCENARIOS).filter(scenario => {
|
|
497
|
+
return scenario !== BASIC_REPORTING_SCENARIO && scenarios.has(scenario)
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Fails closed when orchestration omits a selected check for a runnable framework.
|
|
503
|
+
*
|
|
504
|
+
* @param {object[]} results collected validation results
|
|
505
|
+
* @param {object[]} frameworks runnable frameworks whose live phases started
|
|
506
|
+
* @param {Set<string>} scenarios selected scenarios
|
|
507
|
+
* @returns {void}
|
|
508
|
+
*/
|
|
509
|
+
function addMissingRequiredResults (results, frameworks, scenarios) {
|
|
510
|
+
for (const framework of frameworks) {
|
|
511
|
+
for (const scenario of scenarios) {
|
|
512
|
+
if (results.some(result => result.frameworkId === framework.id && result.scenario === scenario)) continue
|
|
513
|
+
results.push({
|
|
514
|
+
frameworkId: framework.id,
|
|
515
|
+
scenario,
|
|
516
|
+
status: 'error',
|
|
517
|
+
diagnosis: `${getScenarioDisplayName(scenario)} was selected but produced no validation result.`,
|
|
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
|
+
},
|
|
523
|
+
artifacts: [],
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Reports whether all default checks produced results in an unscoped run.
|
|
531
|
+
*
|
|
532
|
+
* @param {object} input coverage inputs
|
|
533
|
+
* @param {object[]} input.results validation results
|
|
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
|
|
538
|
+
*/
|
|
539
|
+
function getValidationCoverage ({ results, requestedScenario, frameworks, scenarios }) {
|
|
540
|
+
if (requestedScenario) return 'partial'
|
|
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'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getSelectableScenarios () {
|
|
558
|
+
return [
|
|
559
|
+
BASIC_REPORTING_SCENARIO,
|
|
560
|
+
CI_WIRING_SCENARIO,
|
|
561
|
+
...Object.keys(SCENARIOS).filter(scenario => scenario !== BASIC_REPORTING_SCENARIO),
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getSkippedCiWiringAfterBasicFailure (framework, basicResult) {
|
|
566
|
+
return {
|
|
567
|
+
frameworkId: framework.id,
|
|
568
|
+
scenario: 'ci-wiring',
|
|
569
|
+
status: 'skip',
|
|
570
|
+
diagnosis: 'Skipped CI wiring validation because Basic Reporting did not pass with direct Datadog ' +
|
|
571
|
+
'initialization. Fix the selected test command or local Test Optimization capability before diagnosing CI ' +
|
|
572
|
+
'wiring.',
|
|
573
|
+
evidence: {
|
|
574
|
+
blockedBy: BASIC_REPORTING_SCENARIO,
|
|
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
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function getSkippedAfterBasicFailure (framework, scenario, basicResult) {
|
|
590
|
+
return {
|
|
591
|
+
frameworkId: framework.id,
|
|
592
|
+
scenario,
|
|
593
|
+
status: 'skip',
|
|
594
|
+
diagnosis: `Skipped because basic reporting did not pass: ${basicResult.diagnosis}`,
|
|
595
|
+
evidence: {
|
|
596
|
+
blockedBy: BASIC_REPORTING_SCENARIO,
|
|
597
|
+
basicReportingStatus: basicResult.status,
|
|
598
|
+
basicReportingDiagnosis: basicResult.diagnosis,
|
|
599
|
+
featureEligibility: {
|
|
600
|
+
eligible: false,
|
|
601
|
+
blockedBy: BASIC_REPORTING_SCENARIO,
|
|
602
|
+
reasonCode: 'basic-reporting-failed',
|
|
603
|
+
scenario,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
artifacts: [],
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function getSkippedAfterGeneratedVerificationFailure (framework, scenario, failure) {
|
|
611
|
+
return {
|
|
612
|
+
frameworkId: framework.id,
|
|
613
|
+
scenario,
|
|
614
|
+
status: 'skip',
|
|
615
|
+
diagnosis: `Skipped because the temporary validation test could not run as expected: ${failure.diagnosis}`,
|
|
616
|
+
evidence: {
|
|
617
|
+
blockedBy: 'generated-test-verification',
|
|
618
|
+
verificationStatus: failure.status,
|
|
619
|
+
verificationDiagnosis: failure.diagnosis,
|
|
620
|
+
featureEligibility: {
|
|
621
|
+
eligible: false,
|
|
622
|
+
blockedBy: 'generated-test-verification',
|
|
623
|
+
reasonCode: 'generated-test-verification-failed',
|
|
624
|
+
scenario,
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
artifacts: [],
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isUnsuccessfulResult (result) {
|
|
632
|
+
return result.status === 'fail' || result.status === 'error' || result.status === 'blocked'
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Reports whether at least one live validation check produced a result.
|
|
637
|
+
*
|
|
638
|
+
* Framework discovery-only entries use the synthetic `all` scenario and do not prove Test Optimization behavior.
|
|
639
|
+
*
|
|
640
|
+
* @param {object[]} results validation results
|
|
641
|
+
* @returns {boolean} whether live validation ran
|
|
642
|
+
*/
|
|
643
|
+
function didRunLiveValidation (results) {
|
|
644
|
+
return results.some(result => result.scenario !== 'all')
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Prints the start of one framework validation phase.
|
|
649
|
+
*
|
|
650
|
+
* @param {object} framework manifest framework entry
|
|
651
|
+
* @param {string} phase customer-facing phase name
|
|
652
|
+
* @returns {void}
|
|
653
|
+
*/
|
|
654
|
+
function logPhaseStart (framework, phase) {
|
|
655
|
+
logValidationProgress(`${framework.id}: ${phase} started.`)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Prints the outcome of one framework validation phase.
|
|
660
|
+
*
|
|
661
|
+
* @param {object} framework manifest framework entry
|
|
662
|
+
* @param {string} phase customer-facing phase name
|
|
663
|
+
* @param {string|undefined} status phase outcome
|
|
664
|
+
* @returns {void}
|
|
665
|
+
*/
|
|
666
|
+
function logPhaseComplete (framework, phase, status) {
|
|
667
|
+
logValidationProgress(`${framework.id}: ${phase} ${status || 'complete'}.`)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Prints a sanitized validator progress line.
|
|
672
|
+
*
|
|
673
|
+
* @param {string} message progress message
|
|
674
|
+
* @returns {void}
|
|
675
|
+
*/
|
|
676
|
+
function logValidationProgress (message) {
|
|
677
|
+
console.log(sanitizeConsoleText(`[test-optimization-validator] ${message}`))
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Converts an advanced scenario id to customer-facing text.
|
|
682
|
+
*
|
|
683
|
+
* @param {string} scenario scenario id
|
|
684
|
+
* @returns {string} display name
|
|
685
|
+
*/
|
|
686
|
+
function getScenarioDisplayName (scenario) {
|
|
687
|
+
return {
|
|
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) {
|
|
697
|
+
return {
|
|
698
|
+
frameworkId: framework.id,
|
|
699
|
+
scenario: 'all',
|
|
700
|
+
status: 'fail',
|
|
701
|
+
diagnosis: blocker.reason,
|
|
702
|
+
evidence: {
|
|
703
|
+
staticDiagnosis: true,
|
|
704
|
+
recommendation: blocker.recommendation,
|
|
705
|
+
},
|
|
706
|
+
artifacts: [staticDiagnosisPath],
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function getFrameworkStatusResult (framework) {
|
|
711
|
+
const evidence = getFrameworkStatusEvidence(framework)
|
|
712
|
+
|
|
713
|
+
if (framework.status === 'unsupported_by_validator') {
|
|
714
|
+
const frameworkName = getDisplayFrameworkName(framework.framework)
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
frameworkId: framework.id,
|
|
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
|
+
|
|
729
|
+
return {
|
|
730
|
+
frameworkId: framework.id,
|
|
731
|
+
scenario: 'all',
|
|
732
|
+
status: 'skip',
|
|
733
|
+
diagnosis: getFrameworkStatusDiagnosis(framework, evidence),
|
|
734
|
+
evidence: {
|
|
735
|
+
...evidence,
|
|
736
|
+
recommendation: 'Provide a small runnable command for this framework, or mark the setup blocker explicitly.',
|
|
737
|
+
},
|
|
738
|
+
artifacts: [],
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function getFrameworkStatusDiagnosis (framework, evidence) {
|
|
743
|
+
const frameworkName = framework.framework
|
|
744
|
+
const notes = evidence.manifestNotes || []
|
|
745
|
+
|
|
746
|
+
if (isDependencyOnlyDetection(evidence)) {
|
|
747
|
+
return getDependencyOnlyDiagnosis(framework, evidence)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (notes.length > 0) {
|
|
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
|
|
800
|
+
return {
|
|
801
|
+
frameworkStatus: framework.status,
|
|
802
|
+
frameworkVersion: framework.frameworkVersion,
|
|
803
|
+
manifestNotes: Array.isArray(framework.notes) ? framework.notes : [],
|
|
804
|
+
directDependency: root ? getDirectDependency(root, framework.framework) : undefined,
|
|
805
|
+
frameworkScripts: root ? findFrameworkScripts(root, framework.framework) : [],
|
|
806
|
+
testLikeScripts: root ? findTestLikeScripts(root) : [],
|
|
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 }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function findFrameworkScripts (root, frameworkName) {
|
|
822
|
+
return findScripts(root, (name, command) => {
|
|
823
|
+
return includesWord(name, frameworkName) || includesWord(command, frameworkName)
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function findTestLikeScripts (root) {
|
|
828
|
+
return findScripts(root, name => /(^|:)(test|unit|e2e|integration|ci)(:|$)/.test(name)).slice(0, 8)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function findScripts (root, predicate) {
|
|
832
|
+
const packageJson = readPackageJson(root)
|
|
833
|
+
const scripts = packageJson?.scripts || {}
|
|
834
|
+
const matches = []
|
|
835
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
836
|
+
if (predicate(name, command)) matches.push({ name, command })
|
|
837
|
+
}
|
|
838
|
+
return matches.slice(0, 8)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function findFrameworkConfigFiles (root, frameworkName) {
|
|
842
|
+
const patterns = getFrameworkConfigPatterns(frameworkName)
|
|
843
|
+
if (patterns.length === 0) return []
|
|
844
|
+
|
|
845
|
+
const files = []
|
|
846
|
+
findFiles(root, 4, file => {
|
|
847
|
+
if (patterns.some(pattern => pattern.test(path.basename(file)))) {
|
|
848
|
+
files.push(path.relative(root, file))
|
|
849
|
+
}
|
|
850
|
+
return files.length < 8
|
|
851
|
+
})
|
|
852
|
+
return files
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function getFrameworkConfigPatterns (frameworkName) {
|
|
856
|
+
const definition = getFrameworkDefinitions(DD_MAJOR).find(definition => definition.id === frameworkName)
|
|
857
|
+
return definition?.configPatterns || []
|
|
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
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function findFiles (dir, depth, visit) {
|
|
869
|
+
if (depth < 0) return true
|
|
870
|
+
|
|
871
|
+
let entries
|
|
872
|
+
try {
|
|
873
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
874
|
+
} catch {
|
|
875
|
+
return true
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
for (const entry of entries) {
|
|
879
|
+
if (shouldSkipDirectory(entry.name)) continue
|
|
880
|
+
const filename = path.join(dir, entry.name)
|
|
881
|
+
if (entry.isDirectory()) {
|
|
882
|
+
if (!findFiles(filename, depth - 1, visit)) return false
|
|
883
|
+
} else if (!visit(filename)) {
|
|
884
|
+
return false
|
|
885
|
+
}
|
|
886
|
+
}
|
|
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
|
+
}
|
|
902
|
+
|
|
903
|
+
module.exports = { filterFrameworks, main, normalizeFrameworkTarget, parseArgs }
|