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,1120 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
const { getArtifactId } = require('./artifact-id')
|
|
8
|
+
const { writeApprovalArtifacts } = require('./approval-artifacts')
|
|
9
|
+
const { getCommandOutputPaths } = require('./command-output-policy')
|
|
10
|
+
const { getCommandSuitabilityError } = require('./command-suitability')
|
|
11
|
+
const { serializeApprovalCommand } = require('./command-runner')
|
|
12
|
+
const {
|
|
13
|
+
getApprovedExecutable,
|
|
14
|
+
getUnavailableExecutable,
|
|
15
|
+
} = require('./executable')
|
|
16
|
+
const { getCiWiringCommand, getDatadogCleanCommand, getLocalValidationCommand } = require('./local-command')
|
|
17
|
+
const {
|
|
18
|
+
getOfflineFixturePaths,
|
|
19
|
+
getOfflineScenarioNames,
|
|
20
|
+
} = require('./offline-fixtures')
|
|
21
|
+
const { sanitizeEnv, sanitizeString } = require('./redaction')
|
|
22
|
+
const { getBasicReportingCommand } = require('./scenarios/basic-reporting')
|
|
23
|
+
const { writeFileSafely } = require('./safe-files')
|
|
24
|
+
|
|
25
|
+
const VALIDATOR_PATH = path.resolve(__dirname, '..', 'validate-test-optimization.js')
|
|
26
|
+
const APPROVAL_SUMMARY_FILENAME = 'approval-summary.md'
|
|
27
|
+
const EXECUTION_PLAN_FILENAME = 'execution-plan.md'
|
|
28
|
+
const GENERATED_SCENARIO_DETAILS = {
|
|
29
|
+
'basic-pass': {
|
|
30
|
+
heading: 'Advanced Check: Early Flake Detection',
|
|
31
|
+
description: 'Creates a temporary passing test, records it with Early Flake Detection disabled, then enables ' +
|
|
32
|
+
'the feature and checks that Datadog recognizes the test as new and retries it.',
|
|
33
|
+
},
|
|
34
|
+
'atr-fail-once': {
|
|
35
|
+
heading: 'Advanced Check: Auto Test Retries',
|
|
36
|
+
description: 'Creates a temporary test that fails on its first attempt, then checks that Datadog retries it ' +
|
|
37
|
+
'and observes the passing attempt.',
|
|
38
|
+
},
|
|
39
|
+
'test-management-target': {
|
|
40
|
+
heading: 'Advanced Check: Test Management',
|
|
41
|
+
description: 'Creates a temporary target test, supplies a quarantine setting for that test, then checks that ' +
|
|
42
|
+
'Datadog applies the setting.',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
const FRAMEWORK_NAMES = {
|
|
46
|
+
jest: 'Jest',
|
|
47
|
+
karma: 'Karma',
|
|
48
|
+
mocha: 'Mocha',
|
|
49
|
+
'node:test': 'Node.js test runner',
|
|
50
|
+
playwright: 'Playwright',
|
|
51
|
+
vitest: 'Vitest',
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line prefer-regex-literals
|
|
54
|
+
const CONTROL_CHARACTERS_PATTERN = new RegExp(String.raw`[\u0000-\u001F\u007F]+`, 'g')
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Produces the deterministic execution plan shown before live validation.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} input plan inputs
|
|
60
|
+
* @param {object} input.manifest normalized validation manifest
|
|
61
|
+
* @param {string} input.out validation output directory
|
|
62
|
+
* @param {string[]} [input.selectedFrameworkIds] explicitly selected framework entries
|
|
63
|
+
* @param {string|null} [input.requestedScenario] explicitly selected scenario
|
|
64
|
+
* @param {boolean} [input.keepTempFiles] whether generated files should be retained
|
|
65
|
+
* @param {boolean} [input.verbose] whether command progress should be printed
|
|
66
|
+
* @returns {string} Markdown execution plan
|
|
67
|
+
*/
|
|
68
|
+
function formatExecutionPlan ({
|
|
69
|
+
manifest,
|
|
70
|
+
out,
|
|
71
|
+
selectedFrameworkIds = [],
|
|
72
|
+
requestedScenario,
|
|
73
|
+
keepTempFiles = false,
|
|
74
|
+
verbose = false,
|
|
75
|
+
}) {
|
|
76
|
+
assertPlannedExecutablesAvailable(manifest, requestedScenario)
|
|
77
|
+
const offlineFixtureNonce = crypto.randomBytes(16).toString('hex')
|
|
78
|
+
const approvalArtifacts = writeApprovalArtifacts({
|
|
79
|
+
manifest,
|
|
80
|
+
out,
|
|
81
|
+
selectedFrameworkIds,
|
|
82
|
+
requestedScenario,
|
|
83
|
+
offlineFixtureNonce,
|
|
84
|
+
keepTempFiles,
|
|
85
|
+
verbose,
|
|
86
|
+
})
|
|
87
|
+
const approvalDigest = approvalArtifacts.digest
|
|
88
|
+
const validatorArgv = getValidatorArgv({
|
|
89
|
+
approvedPlanSha256: approvalDigest,
|
|
90
|
+
approvalJsonPath: approvalArtifacts.approvalJsonPath,
|
|
91
|
+
repositoryRoot: manifest.repository.root,
|
|
92
|
+
})
|
|
93
|
+
const coveredFileVerification = process.platform === 'win32'
|
|
94
|
+
? []
|
|
95
|
+
: [
|
|
96
|
+
'Optional: verify every listed dd-trace package and command executable file against its recorded SHA-256:',
|
|
97
|
+
'',
|
|
98
|
+
codeBlock(sanitizeString(serializeApprovalCommand({
|
|
99
|
+
argv: ['shasum', '-a', '256', '--quiet', '-c', approvalArtifacts.coveredFilesPath],
|
|
100
|
+
cwd: manifest.repository.root,
|
|
101
|
+
usesShell: false,
|
|
102
|
+
}))),
|
|
103
|
+
'',
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const lines = [
|
|
107
|
+
'# Test Optimization Validation Execution Plan',
|
|
108
|
+
'',
|
|
109
|
+
`Repository: ${inlineCode(manifest.repository.root)}`,
|
|
110
|
+
`Manifest: ${inlineCode(manifest.__path)}`,
|
|
111
|
+
`Results: ${inlineCode(out)}`,
|
|
112
|
+
'',
|
|
113
|
+
'## What Will Be Validated',
|
|
114
|
+
'',
|
|
115
|
+
'The validator runs selected project tests without Datadog to confirm they work normally, then runs the same ' +
|
|
116
|
+
'tests with Datadog initialized to check that test data is reported. When a CI test command can be replayed, ' +
|
|
117
|
+
'it also checks whether the configuration from that CI job reaches the test process. Temporary tests are ' +
|
|
118
|
+
'used for the advanced feature checks.',
|
|
119
|
+
'',
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
for (const framework of manifest.frameworks) {
|
|
123
|
+
const label = formatFrameworkLabel(framework, manifest.repository.root)
|
|
124
|
+
lines.push(`- **${plainText(label)}**: ${formatFrameworkStatus(framework.status)}`)
|
|
125
|
+
if (framework.status !== 'runnable') {
|
|
126
|
+
for (const note of framework.notes || []) lines.push(` - ${plainText(note)}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
lines.push(
|
|
131
|
+
'',
|
|
132
|
+
'## Test Commands',
|
|
133
|
+
'',
|
|
134
|
+
'Environment values that affect a project command are shown inline after secret-like values are replaced with ' +
|
|
135
|
+
'`<redacted>`. Repository files in a command are shown relative to its stated working directory. Datadog ' +
|
|
136
|
+
'preloads are shown as package names; the validator resolves them from the installed `dd-trace` package.',
|
|
137
|
+
''
|
|
138
|
+
)
|
|
139
|
+
for (const framework of manifest.frameworks) {
|
|
140
|
+
if (framework.status !== 'runnable') continue
|
|
141
|
+
|
|
142
|
+
appendFrameworkExecutions(
|
|
143
|
+
lines,
|
|
144
|
+
framework,
|
|
145
|
+
requestedScenario,
|
|
146
|
+
manifest.repository.root,
|
|
147
|
+
out,
|
|
148
|
+
offlineFixtureNonce
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
appendCommandIntegrity(lines, manifest, requestedScenario)
|
|
152
|
+
|
|
153
|
+
lines.push(
|
|
154
|
+
'',
|
|
155
|
+
'## Start the Validation',
|
|
156
|
+
'',
|
|
157
|
+
'`validate-test-optimization.js` is the local validator included with the installed `dd-trace` package. ' +
|
|
158
|
+
'After approval, it creates bounded filesystem cache fixtures, performs every check listed above, writes ' +
|
|
159
|
+
'events to local artifacts, and removes temporary fixtures and tests afterward. It does not open a listener ' +
|
|
160
|
+
'or use a network endpoint.',
|
|
161
|
+
'',
|
|
162
|
+
'The validator wrote the exact approval material to these local files without running project code:',
|
|
163
|
+
'',
|
|
164
|
+
`- Approval details: ${inlineCode(getRepositoryRelativePath(
|
|
165
|
+
manifest.repository.root,
|
|
166
|
+
approvalArtifacts.approvalJsonPath
|
|
167
|
+
))}`,
|
|
168
|
+
`- Covered file checksums: ${inlineCode(getRepositoryRelativePath(
|
|
169
|
+
manifest.repository.root,
|
|
170
|
+
approvalArtifacts.coveredFilesPath
|
|
171
|
+
))}`,
|
|
172
|
+
'',
|
|
173
|
+
'The JSON contains the sanitized command shapes, generated test source, selected options, file fingerprints, ' +
|
|
174
|
+
'and executable identities covered by approval. It is an internal diagnostic artifact and may contain ' +
|
|
175
|
+
'repository paths or CI metadata.',
|
|
176
|
+
'',
|
|
177
|
+
'Optional: independently hash the approval JSON with a standard system tool:',
|
|
178
|
+
'',
|
|
179
|
+
codeBlock(formatIndependentHashCommand(approvalArtifacts.approvalJsonPath)),
|
|
180
|
+
'',
|
|
181
|
+
`Expected SHA-256: ${inlineCode(approvalDigest)}`,
|
|
182
|
+
'',
|
|
183
|
+
...coveredFileVerification,
|
|
184
|
+
'Immediately before project code runs, the validator verifies the saved approval JSON against the SHA-256 in ' +
|
|
185
|
+
'the command below, then reconstructs the approval material from the current manifest, validator package, ' +
|
|
186
|
+
'generated tests, and executables. Both checks must match. This detects changes after review; it does not ' +
|
|
187
|
+
'verify where the installed `dd-trace` package came from.',
|
|
188
|
+
'',
|
|
189
|
+
'Run the approved validation command:',
|
|
190
|
+
'',
|
|
191
|
+
codeBlock(sanitizeString(serializeApprovalCommand({
|
|
192
|
+
argv: validatorArgv,
|
|
193
|
+
cwd: manifest.repository.root,
|
|
194
|
+
usesShell: false,
|
|
195
|
+
}))),
|
|
196
|
+
'',
|
|
197
|
+
`Working directory: ${inlineCode(manifest.repository.root)}`,
|
|
198
|
+
'',
|
|
199
|
+
'The validator supplies diagnostic Datadog settings from cache files only while these local checks run; those ' +
|
|
200
|
+
'settings are not customer CI recommendations. dd-trace makes no network requests in this validation mode. ' +
|
|
201
|
+
'A setup or test command may still use the network unless the execution sandbox blocks it.',
|
|
202
|
+
'',
|
|
203
|
+
'These checks run the project commands listed above. The validator does not require real Datadog ' +
|
|
204
|
+
'credentials, inspect credential stores, or upload validation results. Project tests are arbitrary code and ' +
|
|
205
|
+
'can forge diagnostic cache or event data, so this result is diagnostic evidence, not a security attestation. ' +
|
|
206
|
+
'Review the exact commands before approving them for this environment.'
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const plan = lines.join('\n')
|
|
210
|
+
const approvalSummary = formatApprovalSummary({
|
|
211
|
+
approvalArtifacts,
|
|
212
|
+
approvalDigest,
|
|
213
|
+
manifest,
|
|
214
|
+
out,
|
|
215
|
+
requestedScenario,
|
|
216
|
+
validatorArgv,
|
|
217
|
+
})
|
|
218
|
+
writeFileSafely(out, getExecutionPlanPath(out), `${plan}\n`, 'validation execution plan')
|
|
219
|
+
writeFileSafely(out, getApprovalSummaryPath(out), `${approvalSummary}\n`, 'validation approval summary')
|
|
220
|
+
return plan
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns the bounded customer-facing summary an agent presents before approval.
|
|
225
|
+
*
|
|
226
|
+
* @param {object} input summary inputs
|
|
227
|
+
* @param {object} input.approvalArtifacts written approval artifact paths
|
|
228
|
+
* @param {string} input.approvalDigest approval material digest
|
|
229
|
+
* @param {object} input.manifest normalized validation manifest
|
|
230
|
+
* @param {string} input.out validation output directory
|
|
231
|
+
* @param {string|null|undefined} input.requestedScenario selected scenario
|
|
232
|
+
* @param {string[]} input.validatorArgv approved validator command
|
|
233
|
+
* @returns {string} Markdown approval summary
|
|
234
|
+
*/
|
|
235
|
+
function formatApprovalSummary ({
|
|
236
|
+
approvalArtifacts,
|
|
237
|
+
approvalDigest,
|
|
238
|
+
manifest,
|
|
239
|
+
out,
|
|
240
|
+
requestedScenario,
|
|
241
|
+
validatorArgv,
|
|
242
|
+
}) {
|
|
243
|
+
const repositoryRoot = manifest.repository.root
|
|
244
|
+
const coveredFileVerification = process.platform === 'win32'
|
|
245
|
+
? []
|
|
246
|
+
: [
|
|
247
|
+
'Optional: verify every covered manifest, validator, and executable file:',
|
|
248
|
+
'',
|
|
249
|
+
codeBlock(sanitizeString(serializeApprovalCommand({
|
|
250
|
+
argv: ['shasum', '-a', '256', '--quiet', '-c', approvalArtifacts.coveredFilesPath],
|
|
251
|
+
cwd: repositoryRoot,
|
|
252
|
+
usesShell: false,
|
|
253
|
+
}))),
|
|
254
|
+
'',
|
|
255
|
+
]
|
|
256
|
+
const lines = [
|
|
257
|
+
'# Test Optimization Validation Approval Summary',
|
|
258
|
+
'',
|
|
259
|
+
`Repository: ${inlineCode(repositoryRoot)}`,
|
|
260
|
+
`Detailed execution plan: ${inlineCode(getRepositoryRelativePath(repositoryRoot, getExecutionPlanPath(out)))}`,
|
|
261
|
+
'',
|
|
262
|
+
'This summary shows every project command and the exact temporary test source. The detailed plan contains ' +
|
|
263
|
+
'the offline-fixture, artifact, executable-integrity, and checksum details covered by the same approval hash.',
|
|
264
|
+
'',
|
|
265
|
+
'## Scope',
|
|
266
|
+
'',
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
for (const framework of manifest.frameworks) {
|
|
270
|
+
const label = formatFrameworkLabel(framework, repositoryRoot)
|
|
271
|
+
lines.push(`- **${plainText(label)}**: ${formatFrameworkStatus(framework.status)}`)
|
|
272
|
+
if (framework.status !== 'runnable' && framework.notes?.[0]) {
|
|
273
|
+
lines.push(` - ${plainText(framework.notes[0])}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines.push('', '## Commands', '')
|
|
278
|
+
for (const framework of manifest.frameworks) {
|
|
279
|
+
if (framework.status !== 'runnable') continue
|
|
280
|
+
|
|
281
|
+
appendApprovalSummaryFramework(lines, framework, requestedScenario, repositoryRoot)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
lines.push(
|
|
285
|
+
'## Safety and Outputs',
|
|
286
|
+
'',
|
|
287
|
+
`- Local results: ${inlineCode(getRepositoryRelativePath(repositoryRoot, out))}`,
|
|
288
|
+
'- The validator creates private offline Datadog response files outside the repository and removes them ' +
|
|
289
|
+
'afterward.',
|
|
290
|
+
'- The dd-trace validation path opens no listener, contacts no Datadog endpoint, requires no real Datadog ' +
|
|
291
|
+
'credentials, and uploads nothing.',
|
|
292
|
+
'- Project commands are repository code and may use the network or access local resources unless the ' +
|
|
293
|
+
'execution environment prevents it.',
|
|
294
|
+
'',
|
|
295
|
+
'## Approval Command',
|
|
296
|
+
'',
|
|
297
|
+
`Approval details: ${inlineCode(getRepositoryRelativePath(
|
|
298
|
+
repositoryRoot,
|
|
299
|
+
approvalArtifacts.approvalJsonPath
|
|
300
|
+
))}`,
|
|
301
|
+
'',
|
|
302
|
+
'Optional: independently reproduce the approval hash without running project code:',
|
|
303
|
+
'',
|
|
304
|
+
codeBlock(formatIndependentHashCommand(approvalArtifacts.approvalJsonPath)),
|
|
305
|
+
'',
|
|
306
|
+
`Expected SHA-256: ${inlineCode(approvalDigest)}`,
|
|
307
|
+
'',
|
|
308
|
+
...coveredFileVerification,
|
|
309
|
+
'These checks confirm that the reviewed inputs have not changed since plan generation. They do not verify ' +
|
|
310
|
+
'where the installed `dd-trace` package came from; establish package origin separately through trusted ' +
|
|
311
|
+
'lockfile/integrity metadata or a verified package tarball.',
|
|
312
|
+
'',
|
|
313
|
+
'Run the approved validation command:',
|
|
314
|
+
'',
|
|
315
|
+
codeBlock(sanitizeString(serializeApprovalCommand({
|
|
316
|
+
argv: validatorArgv,
|
|
317
|
+
cwd: repositoryRoot,
|
|
318
|
+
usesShell: false,
|
|
319
|
+
}))),
|
|
320
|
+
'',
|
|
321
|
+
`Working directory: ${inlineCode(repositoryRoot)}`,
|
|
322
|
+
'',
|
|
323
|
+
'Approve executing the commands and temporary file operations shown in this summary?'
|
|
324
|
+
)
|
|
325
|
+
return lines.join('\n')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Appends one runnable framework to the bounded approval summary.
|
|
330
|
+
*
|
|
331
|
+
* @param {string[]} lines rendered summary lines
|
|
332
|
+
* @param {object} framework manifest framework entry
|
|
333
|
+
* @param {string|null|undefined} requestedScenario selected scenario
|
|
334
|
+
* @param {string} repositoryRoot repository root
|
|
335
|
+
* @returns {void}
|
|
336
|
+
*/
|
|
337
|
+
function appendApprovalSummaryFramework (lines, framework, requestedScenario, repositoryRoot) {
|
|
338
|
+
const basicCommand = getBasicReportingCommand(framework)
|
|
339
|
+
const directInitialization = getDirectInitialization(framework)
|
|
340
|
+
const maxTestCount = framework.preflight?.maxTestCount ?? 50
|
|
341
|
+
lines.push(`### ${plainText(formatFrameworkLabel(framework, repositoryRoot))}`, '')
|
|
342
|
+
|
|
343
|
+
for (const setupCommand of framework.setup?.commands || []) {
|
|
344
|
+
appendApprovalSummaryCommand(lines, {
|
|
345
|
+
command: setupCommand,
|
|
346
|
+
label: `Project setup: ${setupCommand.id || setupCommand.description || 'setup'}`,
|
|
347
|
+
repositoryRoot,
|
|
348
|
+
runs: '1',
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
appendApprovalSummaryCommand(lines, {
|
|
352
|
+
command: getDatadogCleanCommand(basicCommand),
|
|
353
|
+
label: 'Test execution without Datadog',
|
|
354
|
+
note: 'Inherited NODE_OPTIONS and DD_* variables are removed. The command must report between 1 and ' +
|
|
355
|
+
`${maxTestCount} tests.`,
|
|
356
|
+
repositoryRoot,
|
|
357
|
+
runs: '1, plus 1 clean confirmation only if the Datadog run exits differently',
|
|
358
|
+
})
|
|
359
|
+
appendApprovalSummaryCommand(lines, {
|
|
360
|
+
command: basicCommand,
|
|
361
|
+
environmentOverrides: { NODE_OPTIONS: directInitialization },
|
|
362
|
+
label: 'Test execution with Datadog',
|
|
363
|
+
note: 'A second Datadog debug run occurs only when diagnosis needs debug output.',
|
|
364
|
+
repositoryRoot,
|
|
365
|
+
runs: '1, plus at most 1 Datadog debug run when needed',
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
|
|
369
|
+
if (ciWiringSelected && framework.ciWiringCommand) {
|
|
370
|
+
appendApprovalSummaryCommand(lines, {
|
|
371
|
+
command: getCiWiringCommand(framework),
|
|
372
|
+
label: 'CI test execution',
|
|
373
|
+
note: 'A short preload probe may run when initialization reachability needs confirmation.',
|
|
374
|
+
repositoryRoot,
|
|
375
|
+
runs: '1, plus at most 1 preload probe',
|
|
376
|
+
})
|
|
377
|
+
} else if (ciWiringSelected) {
|
|
378
|
+
lines.push(
|
|
379
|
+
'**CI test execution:** not run.',
|
|
380
|
+
'',
|
|
381
|
+
`Reason: ${plainText(
|
|
382
|
+
framework.ciWiring?.reason || framework.ciWiring?.diagnosis || 'No replayable CI test command was selected.'
|
|
383
|
+
)}`,
|
|
384
|
+
''
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
|
|
389
|
+
const advancedSelected = !requestedScenario || selectedGeneratedScenario
|
|
390
|
+
const strategy = framework.generatedTestStrategy
|
|
391
|
+
if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
|
|
392
|
+
const scenarios = selectedGeneratedScenario
|
|
393
|
+
? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
|
|
394
|
+
: strategy.scenarios || []
|
|
395
|
+
for (const scenario of scenarios) {
|
|
396
|
+
appendApprovalSummaryCommand(lines, {
|
|
397
|
+
command: getLocalValidationCommand(framework, scenario.runCommand),
|
|
398
|
+
environmentOverrides: { NODE_OPTIONS: directInitialization },
|
|
399
|
+
label: GENERATED_SCENARIO_DETAILS[scenario.id]?.heading || `Advanced check: ${scenario.id}`,
|
|
400
|
+
note: 'Runs verification, identity discovery, and feature validation; a debug run occurs only on failure.',
|
|
401
|
+
repositoryRoot,
|
|
402
|
+
runs: '3, or 4 when the debug run is needed',
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
lines.push('**Temporary test source:**', '')
|
|
407
|
+
for (const file of strategy.files || []) {
|
|
408
|
+
lines.push(
|
|
409
|
+
`${inlineCode(getRepositoryRelativePath(repositoryRoot, file.path))}`,
|
|
410
|
+
'',
|
|
411
|
+
codeBlock(file.contentLines.join('\n')),
|
|
412
|
+
''
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
lines.push('**Files removed after validation:**', '')
|
|
416
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
417
|
+
lines.push(`- ${inlineCode(getRepositoryRelativePath(repositoryRoot, cleanupPath))}`)
|
|
418
|
+
}
|
|
419
|
+
lines.push('')
|
|
420
|
+
} else if (advancedSelected && strategy) {
|
|
421
|
+
lines.push(`**Advanced feature checks:** not run. ${plainText(strategy.reason || strategy.status)}`, '')
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Appends one exact command and its execution-relevant context to the approval summary.
|
|
427
|
+
*
|
|
428
|
+
* @param {string[]} lines rendered summary lines
|
|
429
|
+
* @param {object} input command summary
|
|
430
|
+
* @param {object} input.command structured command
|
|
431
|
+
* @param {Record<string, string>} [input.environmentOverrides] validator-provided readable environment
|
|
432
|
+
* @param {string} input.label customer-facing command label
|
|
433
|
+
* @param {string} [input.note] additional execution behavior
|
|
434
|
+
* @param {string} input.repositoryRoot repository root
|
|
435
|
+
* @param {string} input.runs maximum execution count
|
|
436
|
+
* @returns {void}
|
|
437
|
+
*/
|
|
438
|
+
function appendApprovalSummaryCommand (lines, {
|
|
439
|
+
command,
|
|
440
|
+
environmentOverrides = {},
|
|
441
|
+
label,
|
|
442
|
+
note,
|
|
443
|
+
repositoryRoot,
|
|
444
|
+
runs,
|
|
445
|
+
}) {
|
|
446
|
+
lines.push(
|
|
447
|
+
`**${plainText(label)}**`,
|
|
448
|
+
'',
|
|
449
|
+
codeBlock(formatCommandForPlan(command, repositoryRoot, environmentOverrides)),
|
|
450
|
+
'',
|
|
451
|
+
`- Working directory: ${inlineCode(getRepositoryRelativePath(repositoryRoot, command.cwd))}`,
|
|
452
|
+
`- Runs: ${plainText(runs)}`,
|
|
453
|
+
`- Timeout: ${command.timeoutMs || 300_000} ms`
|
|
454
|
+
)
|
|
455
|
+
if (command.usesShell) lines.push(`- Shell executable: ${inlineCode(command.shell || 'platform default shell')}`)
|
|
456
|
+
const outputPaths = getCommandOutputPaths(command)
|
|
457
|
+
if (outputPaths.length > 0) {
|
|
458
|
+
lines.push('- Command-created outputs removed afterward: ' + outputPaths.map(outputPath => {
|
|
459
|
+
return inlineCode(getRepositoryRelativePath(repositoryRoot, outputPath))
|
|
460
|
+
}).join(', '))
|
|
461
|
+
}
|
|
462
|
+
for (const adjustment of command.localAdjustments || []) {
|
|
463
|
+
lines.push(`- Local adjustment: ${plainText(adjustment)}`)
|
|
464
|
+
}
|
|
465
|
+
if (note) lines.push(`- ${plainText(note)}`)
|
|
466
|
+
lines.push('')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Returns the durable customer-facing plan path written by --print-plan.
|
|
471
|
+
*
|
|
472
|
+
* @param {string} out validation output directory
|
|
473
|
+
* @returns {string} absolute execution plan path
|
|
474
|
+
*/
|
|
475
|
+
function getExecutionPlanPath (out) {
|
|
476
|
+
return path.join(out, EXECUTION_PLAN_FILENAME)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Returns the bounded approval summary path written by --print-plan.
|
|
481
|
+
*
|
|
482
|
+
* @param {string} out validation output directory
|
|
483
|
+
* @returns {string} absolute approval summary path
|
|
484
|
+
*/
|
|
485
|
+
function getApprovalSummaryPath (out) {
|
|
486
|
+
return path.join(out, APPROVAL_SUMMARY_FILENAME)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Refuses to render an approvable plan with a command that cannot start before setup runs.
|
|
491
|
+
*
|
|
492
|
+
* @param {object} manifest normalized validation manifest
|
|
493
|
+
* @param {string|null|undefined} requestedScenario selected scenario
|
|
494
|
+
* @returns {void}
|
|
495
|
+
*/
|
|
496
|
+
function assertPlannedExecutablesAvailable (manifest, requestedScenario) {
|
|
497
|
+
for (const framework of manifest.frameworks) {
|
|
498
|
+
if (framework.status !== 'runnable') continue
|
|
499
|
+
|
|
500
|
+
const plannedCommands = getPlannedCommands(framework, requestedScenario)
|
|
501
|
+
for (const plannedCommand of plannedCommands) {
|
|
502
|
+
const executable = getUnavailableExecutable(plannedCommand.command)
|
|
503
|
+
if (!executable) continue
|
|
504
|
+
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Cannot render an approvable plan because ${plannedCommand.label} for ${framework.id} uses ` +
|
|
507
|
+
`executable "${executable}", which is not available from ${plannedCommand.command.cwd}. ` +
|
|
508
|
+
'Choose a locally available command or mark this check with its concrete setup blocker before asking ' +
|
|
509
|
+
'for approval.'
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
for (const plannedCommand of plannedCommands) {
|
|
513
|
+
const suitabilityError = getCommandSuitabilityError({
|
|
514
|
+
command: plannedCommand.command,
|
|
515
|
+
framework,
|
|
516
|
+
label: plannedCommand.label,
|
|
517
|
+
repositoryRoot: manifest.repository.root,
|
|
518
|
+
})
|
|
519
|
+
if (!suitabilityError) continue
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Cannot render an approvable plan because ${plannedCommand.label} for ${framework.id} ${suitabilityError}`
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Collects structured commands selected by the current plan options.
|
|
529
|
+
*
|
|
530
|
+
* @param {object} framework manifest framework entry
|
|
531
|
+
* @param {string|null|undefined} requestedScenario selected scenario
|
|
532
|
+
* @returns {{label: string, command: object}[]} planned commands
|
|
533
|
+
*/
|
|
534
|
+
function getPlannedCommands (framework, requestedScenario) {
|
|
535
|
+
const commands = []
|
|
536
|
+
for (const command of framework.setup?.commands || []) {
|
|
537
|
+
commands.push({ label: `project setup command ${command.id || command.description || ''}`.trim(), command })
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
commands.push({ label: 'the selected test command', command: getBasicReportingCommand(framework) })
|
|
541
|
+
|
|
542
|
+
const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
|
|
543
|
+
if (ciWiringSelected && framework.ciWiringCommand) {
|
|
544
|
+
commands.push({ label: 'the CI test command', command: getCiWiringCommand(framework) })
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
|
|
548
|
+
const advancedSelected = !requestedScenario || selectedGeneratedScenario
|
|
549
|
+
const strategy = framework.generatedTestStrategy
|
|
550
|
+
if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
|
|
551
|
+
const scenarios = selectedGeneratedScenario
|
|
552
|
+
? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
|
|
553
|
+
: strategy.scenarios || []
|
|
554
|
+
for (const scenario of scenarios) {
|
|
555
|
+
commands.push({
|
|
556
|
+
label: `the ${scenario.id} advanced-feature command`,
|
|
557
|
+
command: getLocalValidationCommand(framework, scenario.runCommand),
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return commands
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Builds the exact validator command covered by the approval checkpoint.
|
|
567
|
+
*
|
|
568
|
+
* @param {object} input command options
|
|
569
|
+
* @param {string} input.approvedPlanSha256 digest of the approved manifest and options
|
|
570
|
+
* @param {string} input.approvalJsonPath reviewed approval JSON path
|
|
571
|
+
* @param {string} input.repositoryRoot repository root
|
|
572
|
+
* @returns {string[]} validator argv
|
|
573
|
+
*/
|
|
574
|
+
function getValidatorArgv ({
|
|
575
|
+
approvedPlanSha256,
|
|
576
|
+
approvalJsonPath,
|
|
577
|
+
repositoryRoot,
|
|
578
|
+
}) {
|
|
579
|
+
const validatorPath = getPreferredValidatorPath(repositoryRoot)
|
|
580
|
+
return [
|
|
581
|
+
validatorPath === VALIDATOR_PATH ? process.execPath : 'node',
|
|
582
|
+
validatorPath,
|
|
583
|
+
'--run-approved-plan', getRepositoryRelativePath(repositoryRoot, approvalJsonPath),
|
|
584
|
+
'--sha256', approvedPlanSha256,
|
|
585
|
+
]
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Uses the stable package path when it resolves to this installed validator.
|
|
590
|
+
*
|
|
591
|
+
* @param {string} repositoryRoot repository root
|
|
592
|
+
* @returns {string} relative package path or exact validator path
|
|
593
|
+
*/
|
|
594
|
+
function getPreferredValidatorPath (repositoryRoot) {
|
|
595
|
+
const directPath = path.join(repositoryRoot, 'node_modules', 'dd-trace', 'ci', 'validate-test-optimization.js')
|
|
596
|
+
try {
|
|
597
|
+
if (fs.realpathSync(directPath) === fs.realpathSync(VALIDATOR_PATH)) {
|
|
598
|
+
return path.relative(repositoryRoot, directPath).split(path.sep).join('/')
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
601
|
+
return VALIDATOR_PATH
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Returns a platform-standard command for hashing the saved approval JSON independently of the validator.
|
|
606
|
+
*
|
|
607
|
+
* @param {string} approvalJsonPath absolute approval JSON path
|
|
608
|
+
* @returns {string} printable checksum command
|
|
609
|
+
*/
|
|
610
|
+
function formatIndependentHashCommand (approvalJsonPath) {
|
|
611
|
+
const command = process.platform === 'win32'
|
|
612
|
+
? { argv: ['certutil', '-hashfile', approvalJsonPath, 'SHA256'], cwd: path.dirname(approvalJsonPath) }
|
|
613
|
+
: { argv: ['shasum', '-a', '256', approvalJsonPath], cwd: path.dirname(approvalJsonPath) }
|
|
614
|
+
return sanitizeString(serializeApprovalCommand({ ...command, usesShell: false }))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function appendFrameworkExecutions (
|
|
618
|
+
lines,
|
|
619
|
+
framework,
|
|
620
|
+
requestedScenario,
|
|
621
|
+
repositoryRoot,
|
|
622
|
+
out,
|
|
623
|
+
offlineFixtureNonce
|
|
624
|
+
) {
|
|
625
|
+
const basicCommand = getBasicReportingCommand(framework)
|
|
626
|
+
const frameworkLabel = formatFrameworkLabel(framework, repositoryRoot)
|
|
627
|
+
const directInitialization = getDirectInitialization(framework)
|
|
628
|
+
const maxTestCount = framework.preflight?.maxTestCount ?? 50
|
|
629
|
+
lines.push(`### ${plainText(frameworkLabel)}`, '')
|
|
630
|
+
|
|
631
|
+
for (const setupCommand of framework.setup?.commands || []) {
|
|
632
|
+
appendExecutionSection(lines, {
|
|
633
|
+
heading: `Project Setup: ${setupCommand.id || setupCommand.description || 'Project Setup'}`,
|
|
634
|
+
description: 'Prepares the project for the selected test command.',
|
|
635
|
+
command: setupCommand,
|
|
636
|
+
executions: '1',
|
|
637
|
+
environment: 'Use the command-specific variables shown inline. No Datadog variables are added.',
|
|
638
|
+
repositoryRoot,
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
const cleanCommand = getDatadogCleanCommand(basicCommand)
|
|
642
|
+
appendExecutionSection(lines, {
|
|
643
|
+
heading: 'Test Execution Without Datadog',
|
|
644
|
+
description: 'Runs the selected test command without Datadog to confirm that the tests can run normally and ' +
|
|
645
|
+
`that it reports between 1 and ${maxTestCount} tests.`,
|
|
646
|
+
command: cleanCommand,
|
|
647
|
+
executions: '1, plus 1 clean confirmation only if the Datadog run exits differently',
|
|
648
|
+
environment: `Remove inherited NODE_OPTIONS and DD_*; ${formatCommandVariableContext(cleanCommand)}`,
|
|
649
|
+
repositoryRoot,
|
|
650
|
+
})
|
|
651
|
+
appendExecutionSection(lines, {
|
|
652
|
+
heading: 'Test Execution With Datadog',
|
|
653
|
+
description: 'Runs the same test command with Datadog initialized and checks that test data is reported.',
|
|
654
|
+
command: basicCommand,
|
|
655
|
+
executions: '1, plus at most 1 debug rerun when needed',
|
|
656
|
+
environment: 'Datadog initialization is shown inline. The validator also supplies private offline response ' +
|
|
657
|
+
'paths and noise-suppression settings only while this check runs.',
|
|
658
|
+
environmentOverrides: { NODE_OPTIONS: directInitialization },
|
|
659
|
+
repositoryRoot,
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
|
|
663
|
+
if (ciWiringSelected && framework.ciWiringCommand) {
|
|
664
|
+
const ciCommand = getCiWiringCommand(framework)
|
|
665
|
+
appendExecutionSection(lines, {
|
|
666
|
+
heading: 'CI Test Execution',
|
|
667
|
+
description: 'Runs the test command with the environment recorded from the identified CI job and checks ' +
|
|
668
|
+
'whether that CI configuration initializes Datadog in the final test process.',
|
|
669
|
+
command: ciCommand,
|
|
670
|
+
executions: '1, plus 1 short preload probe when needed',
|
|
671
|
+
environment: formatCiEnvironmentSummary(ciCommand),
|
|
672
|
+
repositoryRoot,
|
|
673
|
+
})
|
|
674
|
+
} else if (ciWiringSelected) {
|
|
675
|
+
lines.push(
|
|
676
|
+
'#### CI Test Execution',
|
|
677
|
+
'',
|
|
678
|
+
'Not run.',
|
|
679
|
+
'',
|
|
680
|
+
`- Reason: ${plainText(
|
|
681
|
+
framework.ciWiring?.reason || framework.ciWiring?.diagnosis || 'No replayable CI test command was selected.'
|
|
682
|
+
)}`,
|
|
683
|
+
''
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const strategy = framework.generatedTestStrategy
|
|
688
|
+
const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
|
|
689
|
+
const advancedSelected = !requestedScenario || selectedGeneratedScenario
|
|
690
|
+
if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
|
|
691
|
+
const selectedScenarios = selectedGeneratedScenario
|
|
692
|
+
? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
|
|
693
|
+
: strategy.scenarios || []
|
|
694
|
+
for (const scenario of selectedScenarios) {
|
|
695
|
+
const command = getLocalValidationCommand(framework, scenario.runCommand)
|
|
696
|
+
const details = GENERATED_SCENARIO_DETAILS[scenario.id] || {
|
|
697
|
+
heading: `Advanced Check: ${scenario.id}`,
|
|
698
|
+
description: 'Runs a temporary test to verify this advanced feature.',
|
|
699
|
+
}
|
|
700
|
+
appendExecutionSection(lines, {
|
|
701
|
+
heading: details.heading,
|
|
702
|
+
description: `${details.description} The framework runner is invoked directly so only this temporary test ` +
|
|
703
|
+
'runs instead of the broader project test suite.',
|
|
704
|
+
command,
|
|
705
|
+
executions: '3: verify the test alone, discover its identity, then validate the feature; ' +
|
|
706
|
+
'plus 1 debug rerun only on failure',
|
|
707
|
+
environment: 'Datadog initialization is shown inline. The validator supplies the feature setting from a ' +
|
|
708
|
+
'private offline response only while this check runs.',
|
|
709
|
+
environmentOverrides: { NODE_OPTIONS: directInitialization },
|
|
710
|
+
repositoryRoot,
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
appendOfflineArtifacts(lines, {
|
|
716
|
+
offlineFixtureNonce,
|
|
717
|
+
framework,
|
|
718
|
+
out,
|
|
719
|
+
repositoryRoot,
|
|
720
|
+
requestedScenario,
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
|
|
724
|
+
lines.push(
|
|
725
|
+
'#### Temporary Tests Created for Advanced Checks',
|
|
726
|
+
'',
|
|
727
|
+
'The validator creates these tests temporarily and removes them after validation.',
|
|
728
|
+
''
|
|
729
|
+
)
|
|
730
|
+
for (const file of strategy.files || []) {
|
|
731
|
+
lines.push(
|
|
732
|
+
`##### ${inlineCode(getRepositoryRelativePath(repositoryRoot, file.path))}`,
|
|
733
|
+
'',
|
|
734
|
+
codeBlock(file.contentLines.join('\n')),
|
|
735
|
+
''
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
lines.push(
|
|
739
|
+
'#### Temporary Test Cleanup',
|
|
740
|
+
'',
|
|
741
|
+
'The validator removes these temporary test and state files after validation. Paths are relative to the ' +
|
|
742
|
+
'repository root:',
|
|
743
|
+
''
|
|
744
|
+
)
|
|
745
|
+
for (const cleanupPath of strategy.cleanupPaths || []) {
|
|
746
|
+
lines.push(`- ${inlineCode(getRepositoryRelativePath(repositoryRoot, cleanupPath))}`)
|
|
747
|
+
}
|
|
748
|
+
lines.push('', 'Directories created for these files are also removed when they are empty.', '')
|
|
749
|
+
} else if (advancedSelected && strategy) {
|
|
750
|
+
lines.push(
|
|
751
|
+
'#### Advanced Feature Checks',
|
|
752
|
+
'',
|
|
753
|
+
'Not run.',
|
|
754
|
+
'',
|
|
755
|
+
`- Reason: ${plainText(strategy.reason || strategy.status)}`,
|
|
756
|
+
''
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
lines.push('')
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Describes the offline Datadog responses and event outputs used by instrumented executions.
|
|
764
|
+
*
|
|
765
|
+
* @param {string[]} lines rendered plan lines
|
|
766
|
+
* @param {object} input plan inputs
|
|
767
|
+
* @param {string} input.offlineFixtureNonce random fixture-root nonce
|
|
768
|
+
* @param {object} input.framework framework manifest entry
|
|
769
|
+
* @param {string} input.out validation result directory
|
|
770
|
+
* @param {string} input.repositoryRoot repository root
|
|
771
|
+
* @param {string|null|undefined} input.requestedScenario selected scenario
|
|
772
|
+
*/
|
|
773
|
+
function appendOfflineArtifacts (lines, {
|
|
774
|
+
offlineFixtureNonce,
|
|
775
|
+
framework,
|
|
776
|
+
out,
|
|
777
|
+
repositoryRoot,
|
|
778
|
+
requestedScenario,
|
|
779
|
+
}) {
|
|
780
|
+
lines.push(
|
|
781
|
+
'#### Offline Datadog Responses',
|
|
782
|
+
'',
|
|
783
|
+
'During normal operation, `dd-trace` downloads Test Optimization settings and test lists from Datadog. ' +
|
|
784
|
+
'For this offline validation, the validator writes equivalent bounded responses to a private temporary ' +
|
|
785
|
+
'directory and `dd-trace` reads them from the filesystem. No Datadog backend, Agent, or network endpoint ' +
|
|
786
|
+
'is used.',
|
|
787
|
+
''
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
const scenarioNames = getOfflineScenarioNames(requestedScenario)
|
|
791
|
+
const firstFixture = getOfflineFixturePaths({
|
|
792
|
+
offlineFixtureNonce,
|
|
793
|
+
framework,
|
|
794
|
+
scenarioName: scenarioNames[0],
|
|
795
|
+
})
|
|
796
|
+
const frameworkFixtureRoot = path.dirname(firstFixture.root)
|
|
797
|
+
lines.push(
|
|
798
|
+
`Private response directory: ${inlineCode(frameworkFixtureRoot)}`,
|
|
799
|
+
'',
|
|
800
|
+
'Each check gets an isolated subdirectory containing bounded Test Optimization settings and test lists that ' +
|
|
801
|
+
'`dd-trace` normally receives from Datadog. Isolation prevents a baseline, feature check, or conditional ' +
|
|
802
|
+
'debug rerun from overwriting another check. A debug rerun uses the same response data and adds ' +
|
|
803
|
+
'`DD_TRACE_DEBUG=1`.',
|
|
804
|
+
'',
|
|
805
|
+
`Captured event artifacts: ${inlineCode(getRepositoryRelativePath(
|
|
806
|
+
repositoryRoot,
|
|
807
|
+
path.join(out, 'runs', getArtifactId(framework.id))
|
|
808
|
+
))}`,
|
|
809
|
+
'',
|
|
810
|
+
'Each execution writes bounded temporary JSON payload files under `.offline-payloads/payloads/tests/`, using ' +
|
|
811
|
+
'the Test Optimization payload-file layout. The temporary payload directory is removed after parsing, and a ' +
|
|
812
|
+
'sanitized `events.ndjson` file remains for diagnosis. Exact fixture recipes and paths are included in the ' +
|
|
813
|
+
'approval digest even though this plan summarizes their shared layout.',
|
|
814
|
+
''
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Renders one customer-facing validation step with its exact command and execution context.
|
|
820
|
+
*
|
|
821
|
+
* @param {string[]} lines rendered plan lines
|
|
822
|
+
* @param {object} input execution details
|
|
823
|
+
* @param {string} input.heading customer-facing check name
|
|
824
|
+
* @param {string} input.description reason for the execution
|
|
825
|
+
* @param {object} input.command manifest command
|
|
826
|
+
* @param {string} input.executions maximum execution count
|
|
827
|
+
* @param {string} input.environment environment changes made for this check
|
|
828
|
+
* @param {Record<string, string>} [input.environmentOverrides] readable validator-provided environment values
|
|
829
|
+
* @param {string} input.repositoryRoot repository root
|
|
830
|
+
* @returns {void}
|
|
831
|
+
*/
|
|
832
|
+
function appendExecutionSection (lines, {
|
|
833
|
+
heading,
|
|
834
|
+
description,
|
|
835
|
+
command,
|
|
836
|
+
executions,
|
|
837
|
+
environment,
|
|
838
|
+
environmentOverrides = {},
|
|
839
|
+
repositoryRoot,
|
|
840
|
+
}) {
|
|
841
|
+
lines.push(
|
|
842
|
+
`#### ${plainText(heading)}`,
|
|
843
|
+
'',
|
|
844
|
+
plainText(description),
|
|
845
|
+
'',
|
|
846
|
+
'Command:',
|
|
847
|
+
'',
|
|
848
|
+
codeBlock(formatCommandForPlan(command, repositoryRoot, environmentOverrides)),
|
|
849
|
+
'',
|
|
850
|
+
`- Working directory: ${inlineCode(getRepositoryRelativePath(repositoryRoot, command.cwd))}`,
|
|
851
|
+
`- Runs: ${plainText(executions)}`,
|
|
852
|
+
`- Environment changes: ${plainText(environment)}`,
|
|
853
|
+
`- Timeout: ${command.timeoutMs || 300_000} ms`
|
|
854
|
+
)
|
|
855
|
+
if (command.usesShell) lines.push(`- Shell executable: ${inlineCode(command.shell || 'platform default shell')}`)
|
|
856
|
+
const outputPaths = getCommandOutputPaths(command)
|
|
857
|
+
if (outputPaths.length > 0) {
|
|
858
|
+
lines.push('- Command-created outputs: ' + outputPaths.map(outputPath => {
|
|
859
|
+
return inlineCode(getRepositoryRelativePath(repositoryRoot, outputPath))
|
|
860
|
+
}).join(', ') + ' (must not exist before validation; newly created paths are removed)')
|
|
861
|
+
}
|
|
862
|
+
for (const adjustment of command.localAdjustments || []) {
|
|
863
|
+
lines.push(`- Local adjustment: ${plainText(adjustment)}`)
|
|
864
|
+
}
|
|
865
|
+
lines.push('')
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Shortens a validated repository path for customer-facing plans.
|
|
870
|
+
*
|
|
871
|
+
* @param {string} repositoryRoot repository root shown at the start of the plan
|
|
872
|
+
* @param {string} filename absolute validated path
|
|
873
|
+
* @returns {string} repository-relative path when possible
|
|
874
|
+
*/
|
|
875
|
+
function getRepositoryRelativePath (repositoryRoot, filename) {
|
|
876
|
+
const relative = path.relative(repositoryRoot, filename)
|
|
877
|
+
if (!relative) return '.'
|
|
878
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return filename
|
|
879
|
+
return relative.split(path.sep).join('/')
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function getSelectedGeneratedScenario (requestedScenario) {
|
|
883
|
+
return {
|
|
884
|
+
efd: 'basic-pass',
|
|
885
|
+
atr: 'atr-fail-once',
|
|
886
|
+
'test-management': 'test-management-target',
|
|
887
|
+
}[requestedScenario]
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Describes command-specific variables without displaying their values.
|
|
892
|
+
*
|
|
893
|
+
* @param {object} command manifest command
|
|
894
|
+
* @returns {string} variable context
|
|
895
|
+
*/
|
|
896
|
+
function formatCommandVariableContext (command) {
|
|
897
|
+
const names = Object.keys(command.env || {})
|
|
898
|
+
return names.length > 0
|
|
899
|
+
? `keep command variables: ${names.join(', ')}`
|
|
900
|
+
: 'the command sets no other environment variables'
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Renders the command and its command-specific environment in one readable block.
|
|
905
|
+
*
|
|
906
|
+
* @param {object} command manifest command
|
|
907
|
+
* @param {string} repositoryRoot absolute repository root
|
|
908
|
+
* @param {Record<string, string>} environmentOverrides readable validator-provided values
|
|
909
|
+
* @returns {string} readable command
|
|
910
|
+
*/
|
|
911
|
+
function formatCommandForPlan (command, repositoryRoot, environmentOverrides) {
|
|
912
|
+
const displayCommand = command.usesShell
|
|
913
|
+
? command
|
|
914
|
+
: {
|
|
915
|
+
...command,
|
|
916
|
+
argv: command.argv.map((argument, index) => {
|
|
917
|
+
return formatCommandArgument(argument, index, command, repositoryRoot)
|
|
918
|
+
}),
|
|
919
|
+
}
|
|
920
|
+
const environment = { ...command.env, ...environmentOverrides }
|
|
921
|
+
if (command.env?.NODE_OPTIONS && environmentOverrides.NODE_OPTIONS) {
|
|
922
|
+
environment.NODE_OPTIONS = `${environmentOverrides.NODE_OPTIONS} ${command.env.NODE_OPTIONS}`
|
|
923
|
+
}
|
|
924
|
+
const sanitizedEnvironment = sanitizeEnv(environment) || {}
|
|
925
|
+
const prefix = Object.entries(sanitizedEnvironment).map(([name, value]) => {
|
|
926
|
+
return `${name}=${formatEnvironmentValue(value)}`
|
|
927
|
+
}).join(' ')
|
|
928
|
+
const serialized = sanitizeString(serializeApprovalCommand(displayCommand))
|
|
929
|
+
return prefix ? `${prefix} ${serialized}` : serialized
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Shortens repository-contained command paths without changing their meaning from the stated working directory.
|
|
934
|
+
*
|
|
935
|
+
* @param {string} argument command argument
|
|
936
|
+
* @param {number} index argument index
|
|
937
|
+
* @param {object} command manifest command
|
|
938
|
+
* @param {string} repositoryRoot absolute repository root
|
|
939
|
+
* @returns {string} readable argument
|
|
940
|
+
*/
|
|
941
|
+
function formatCommandArgument (argument, index, command, repositoryRoot) {
|
|
942
|
+
if (index === 0 && path.resolve(argument) === path.resolve(process.execPath)) return 'node'
|
|
943
|
+
if (!path.isAbsolute(argument) || !isPathInside(repositoryRoot, argument)) return argument
|
|
944
|
+
return path.relative(command.cwd, argument) || '.'
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Quotes one environment value only when its characters require it.
|
|
949
|
+
*
|
|
950
|
+
* @param {string} value environment value
|
|
951
|
+
* @returns {string} readable assignment value
|
|
952
|
+
*/
|
|
953
|
+
function formatEnvironmentValue (value) {
|
|
954
|
+
return serializeApprovalCommand({ argv: [String(value)], usesShell: false })
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Checks whether a path remains within a parent directory.
|
|
959
|
+
*
|
|
960
|
+
* @param {string} parent parent directory
|
|
961
|
+
* @param {string} child candidate child path
|
|
962
|
+
* @returns {boolean} whether child is within parent
|
|
963
|
+
*/
|
|
964
|
+
function isPathInside (parent, child) {
|
|
965
|
+
const relative = path.relative(parent, child)
|
|
966
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Returns the customer-facing Datadog preload required by one framework.
|
|
971
|
+
*
|
|
972
|
+
* @param {object} framework manifest framework entry
|
|
973
|
+
* @returns {string} NODE_OPTIONS value
|
|
974
|
+
*/
|
|
975
|
+
function getDirectInitialization (framework) {
|
|
976
|
+
return framework.framework === 'vitest'
|
|
977
|
+
? '--import dd-trace/register.js -r dd-trace/ci/init'
|
|
978
|
+
: '-r dd-trace/ci/init'
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Describes the variables captured from the selected CI job.
|
|
983
|
+
*
|
|
984
|
+
* @param {object} command CI test command
|
|
985
|
+
* @returns {string} customer-facing environment summary
|
|
986
|
+
*/
|
|
987
|
+
function formatCiEnvironmentSummary (command) {
|
|
988
|
+
const names = Object.keys(command.env || {})
|
|
989
|
+
const datadogNames = names.filter(name => name.startsWith('DD_') || name === 'NODE_OPTIONS')
|
|
990
|
+
if (datadogNames.length === 0) {
|
|
991
|
+
return 'The selected CI job supplies no Datadog variables. Other recorded CI variables, if any, are shown ' +
|
|
992
|
+
'inline.'
|
|
993
|
+
}
|
|
994
|
+
return 'Variables recorded from the selected CI job are shown inline. Secret-like values are redacted.'
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Names a framework using the package or project that contributors recognize.
|
|
999
|
+
*
|
|
1000
|
+
* @param {object} framework manifest framework entry
|
|
1001
|
+
* @param {string} repositoryRoot absolute repository root
|
|
1002
|
+
* @returns {string} customer-facing framework label
|
|
1003
|
+
*/
|
|
1004
|
+
function formatFrameworkLabel (framework, repositoryRoot) {
|
|
1005
|
+
const frameworkName = FRAMEWORK_NAMES[framework.framework] || framework.framework || 'Test'
|
|
1006
|
+
const projectName = framework.project?.name
|
|
1007
|
+
if (projectName && projectName !== 'root') return `${frameworkName} tests for ${projectName}`
|
|
1008
|
+
const projectRoot = framework.project?.root
|
|
1009
|
+
const relativeRoot = projectRoot && getRepositoryRelativePath(repositoryRoot, projectRoot)
|
|
1010
|
+
return relativeRoot && relativeRoot !== '.'
|
|
1011
|
+
? `${frameworkName} tests in ${relativeRoot}`
|
|
1012
|
+
: `${frameworkName} tests`
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Lists each executable identity once instead of repeating it under every command.
|
|
1017
|
+
*
|
|
1018
|
+
* @param {string[]} lines rendered plan lines
|
|
1019
|
+
* @param {object} manifest normalized validation manifest
|
|
1020
|
+
* @param {string|null|undefined} requestedScenario selected scenario
|
|
1021
|
+
* @returns {void}
|
|
1022
|
+
*/
|
|
1023
|
+
function appendCommandIntegrity (lines, manifest, requestedScenario) {
|
|
1024
|
+
const executables = new Map()
|
|
1025
|
+
for (const framework of manifest.frameworks) {
|
|
1026
|
+
if (framework.status !== 'runnable') continue
|
|
1027
|
+
|
|
1028
|
+
for (const { command } of getPlannedCommands(framework, requestedScenario)) {
|
|
1029
|
+
const executable = getApprovedExecutable(command)
|
|
1030
|
+
if (executable) {
|
|
1031
|
+
for (const identity of [executable, ...(executable.delegated || [])]) {
|
|
1032
|
+
const key = `${identity.invocationPath}:${identity.path}:${identity.sha256}`
|
|
1033
|
+
const entry = executables.get(key) || { executable: identity, labels: new Set() }
|
|
1034
|
+
entry.labels.add(getExecutableLabel(command, identity.invocationPath))
|
|
1035
|
+
executables.set(key, entry)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (executables.size === 0) return
|
|
1041
|
+
|
|
1042
|
+
lines.push(
|
|
1043
|
+
'## Executables Used',
|
|
1044
|
+
'',
|
|
1045
|
+
'These programs start the commands shown above. The validator records their fingerprints internally and ' +
|
|
1046
|
+
'stops if an executable or PATH selection changes after approval. This confirms that the approved programs ' +
|
|
1047
|
+
'did not change; it does not establish that project scripts, packages, or subprocesses are safe.',
|
|
1048
|
+
''
|
|
1049
|
+
)
|
|
1050
|
+
for (const { executable, labels } of executables.values()) {
|
|
1051
|
+
const canonicalTarget = executable.invocationPath === executable.path
|
|
1052
|
+
? ''
|
|
1053
|
+
: ` (verified target: ${inlineCode(executable.path)})`
|
|
1054
|
+
lines.push(`- ${[...labels].sort().join(', ')}: ${inlineCode(executable.invocationPath)}${canonicalTarget}`)
|
|
1055
|
+
}
|
|
1056
|
+
lines.push('')
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function getExecutableLabel (command, invocationPath) {
|
|
1060
|
+
const name = path.basename(invocationPath).replace(/\.(?:bat|cmd|exe)$/i, '').toLowerCase()
|
|
1061
|
+
if (command.usesShell) {
|
|
1062
|
+
return {
|
|
1063
|
+
bash: 'Bash shell',
|
|
1064
|
+
sh: 'POSIX shell',
|
|
1065
|
+
zsh: 'Zsh shell',
|
|
1066
|
+
}[name] || `${name} shell`
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
bash: 'Bash shell',
|
|
1070
|
+
node: 'Node.js',
|
|
1071
|
+
npm: 'npm',
|
|
1072
|
+
npx: 'npx',
|
|
1073
|
+
pnpm: 'pnpm',
|
|
1074
|
+
sh: 'POSIX shell',
|
|
1075
|
+
yarn: 'Yarn',
|
|
1076
|
+
zsh: 'Zsh shell',
|
|
1077
|
+
}[name] || name
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Converts manifest framework statuses into customer-facing plan text.
|
|
1082
|
+
*
|
|
1083
|
+
* @param {string} status manifest framework status
|
|
1084
|
+
* @returns {string} customer-facing status
|
|
1085
|
+
*/
|
|
1086
|
+
function formatFrameworkStatus (status) {
|
|
1087
|
+
return {
|
|
1088
|
+
runnable: 'will be validated',
|
|
1089
|
+
detected_not_runnable: 'detected, but no runnable command was found',
|
|
1090
|
+
requires_external_service: 'requires an external service',
|
|
1091
|
+
requires_manual_setup: 'requires additional setup',
|
|
1092
|
+
unsupported_by_validator: 'not supported by this validator',
|
|
1093
|
+
unknown: 'could not be determined',
|
|
1094
|
+
}[status] || plainText(status)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function codeBlock (value) {
|
|
1098
|
+
return `\`\`\`text\n${visibleMultilineText(value).replaceAll('```', String.raw`\u0060\u0060\u0060`)}\n\`\`\``
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function inlineCode (value) {
|
|
1102
|
+
return `\`${plainText(value).replaceAll('`', String.raw`\u0060`)}\``
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function plainText (value) {
|
|
1106
|
+
return sanitizeString(String(value ?? '')).replaceAll(CONTROL_CHARACTERS_PATTERN, ' ').trim()
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function visibleMultilineText (value) {
|
|
1110
|
+
return String(value ?? '')
|
|
1111
|
+
.replaceAll('\r\n', '\n')
|
|
1112
|
+
.replaceAll('\r', String.raw`\r`)
|
|
1113
|
+
// eslint-disable-next-line prefer-regex-literals
|
|
1114
|
+
.replaceAll(new RegExp(String.raw`[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]`, 'g'), character => {
|
|
1115
|
+
return String.raw`\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
1116
|
+
})
|
|
1117
|
+
.trim()
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
module.exports = { formatExecutionPlan, getApprovalSummaryPath, getExecutionPlanPath }
|