dd-trace 6.4.0 → 6.5.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 +10 -7
- 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 +20 -7
- 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/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 +26 -0
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- 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 +159 -22
- 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 +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- 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 +1 -1
- 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-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- 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/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,862 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const { getArtifactId } = require('./artifact-id')
|
|
6
|
+
const {
|
|
7
|
+
MAX_GENERATED_FILES,
|
|
8
|
+
getGeneratedFileContentError,
|
|
9
|
+
} = require('./generated-file-policy')
|
|
10
|
+
const { getInlineDatadogInitialization } = require('./local-command')
|
|
11
|
+
const {
|
|
12
|
+
hasUnsafeExecutionCharacter,
|
|
13
|
+
hasUnsafeInvisibleCharacter,
|
|
14
|
+
isSensitiveName,
|
|
15
|
+
sanitizeForReport,
|
|
16
|
+
sanitizeString,
|
|
17
|
+
} = require('./redaction')
|
|
18
|
+
|
|
19
|
+
const FRAMEWORKS = new Set([
|
|
20
|
+
'jest',
|
|
21
|
+
'vitest',
|
|
22
|
+
'mocha',
|
|
23
|
+
'cucumber',
|
|
24
|
+
'cypress',
|
|
25
|
+
'playwright',
|
|
26
|
+
'node:test',
|
|
27
|
+
'ava',
|
|
28
|
+
'tap',
|
|
29
|
+
'jasmine',
|
|
30
|
+
'karma',
|
|
31
|
+
'uvu',
|
|
32
|
+
'testcafe',
|
|
33
|
+
'custom',
|
|
34
|
+
'unknown',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const STATUSES = new Set([
|
|
38
|
+
'runnable',
|
|
39
|
+
'detected_not_runnable',
|
|
40
|
+
'requires_external_service',
|
|
41
|
+
'requires_manual_setup',
|
|
42
|
+
'unsupported_by_validator',
|
|
43
|
+
'unknown',
|
|
44
|
+
])
|
|
45
|
+
const CI_WIRING_STATUSES = new Set([
|
|
46
|
+
'pass',
|
|
47
|
+
'fail',
|
|
48
|
+
'skip',
|
|
49
|
+
'unknown',
|
|
50
|
+
])
|
|
51
|
+
const CI_WIRING_REPLAYABILITIES = new Set(['replayable', 'not_replayable'])
|
|
52
|
+
const CI_INITIALIZATION_STATUSES = new Set(['configured', 'not_configured', 'unknown'])
|
|
53
|
+
const UNRESOLVED_PLACEHOLDER_PATTERN = /\$\{[^}]+\}/
|
|
54
|
+
const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
55
|
+
const MAX_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
|
|
56
|
+
const MAX_FRAMEWORKS = 100
|
|
57
|
+
const MAX_MANIFEST_ARRAY_ENTRIES = 1000
|
|
58
|
+
const MAX_SETUP_COMMANDS = 100
|
|
59
|
+
const MAX_VALIDATION_ERRORS = 50
|
|
60
|
+
const MAX_REPRESENTATIVE_TESTS = 1000
|
|
61
|
+
const SECRET_PLACEHOLDER = 'dd-validation-placeholder'
|
|
62
|
+
const SAFE_SECRET_FIELD_VALUES = new Set(['', '0', '1', 'false', 'true', 'none', 'disabled'])
|
|
63
|
+
const COMMAND_FIELDS = new Set([
|
|
64
|
+
'argv',
|
|
65
|
+
'cwd',
|
|
66
|
+
'description',
|
|
67
|
+
'env',
|
|
68
|
+
'required',
|
|
69
|
+
'requiredEnvVars',
|
|
70
|
+
'shell',
|
|
71
|
+
'shellCommand',
|
|
72
|
+
'shellReason',
|
|
73
|
+
'timeoutMs',
|
|
74
|
+
'usesShell',
|
|
75
|
+
'id',
|
|
76
|
+
'outputPaths',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
const GENERATED_SCENARIO_IDS = new Set([
|
|
80
|
+
'basic-pass',
|
|
81
|
+
'atr-fail-once',
|
|
82
|
+
'test-management-target',
|
|
83
|
+
])
|
|
84
|
+
const GENERATED_SCENARIO_EXIT_CODES = {
|
|
85
|
+
'basic-pass': 0,
|
|
86
|
+
'atr-fail-once': 1,
|
|
87
|
+
'test-management-target': 0,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateManifest (manifest) {
|
|
91
|
+
const errors = createErrorCollector()
|
|
92
|
+
|
|
93
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
94
|
+
return ['Manifest must be a JSON object.']
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
requiredString(manifest, 'schemaVersion', errors)
|
|
98
|
+
requiredObject(manifest, 'repository', errors)
|
|
99
|
+
requiredObject(manifest, 'environment', errors)
|
|
100
|
+
requiredArray(manifest, 'frameworks', errors)
|
|
101
|
+
if (Array.isArray(manifest.frameworks) && manifest.frameworks.length === 0) {
|
|
102
|
+
errors.push('frameworks must include at least one framework entry.')
|
|
103
|
+
}
|
|
104
|
+
validateArrayLimit(manifest, 'frameworks', MAX_FRAMEWORKS, errors)
|
|
105
|
+
validateArrayLimit(manifest, 'omitted', MAX_MANIFEST_ARRAY_ENTRIES, errors)
|
|
106
|
+
validateArrayLimit(manifest, 'omittedTestCommands', MAX_MANIFEST_ARRAY_ENTRIES, errors)
|
|
107
|
+
|
|
108
|
+
if (manifest.repository) {
|
|
109
|
+
requiredAbsolutePath(manifest.repository, 'root', errors)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (manifest.ciDiscovery) {
|
|
113
|
+
validateCiDiscovery(manifest.ciDiscovery, 'ciDiscovery', errors)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Array.isArray(manifest.frameworks)) {
|
|
117
|
+
const frameworks = manifest.frameworks.slice(0, MAX_FRAMEWORKS)
|
|
118
|
+
validateUniqueFrameworkIds(frameworks, errors)
|
|
119
|
+
validateUniqueArtifactIds(frameworks, errors)
|
|
120
|
+
validateDuplicateRunnableCoverage(frameworks, errors)
|
|
121
|
+
validateGeneratedPathCollisions(frameworks, errors)
|
|
122
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
123
|
+
validateFramework(framework, index, errors)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
validateRepositoryContainedPaths(manifest, errors)
|
|
128
|
+
|
|
129
|
+
return errors.finalize()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Rejects generated files or cleanup targets shared by multiple framework entries.
|
|
134
|
+
*
|
|
135
|
+
* @param {object[]} frameworks manifest framework entries
|
|
136
|
+
* @param {{push: function(string): void}} errors bounded validation error collector
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
function validateGeneratedPathCollisions (frameworks, errors) {
|
|
140
|
+
const seen = new Map()
|
|
141
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
142
|
+
const strategy = framework?.generatedTestStrategy
|
|
143
|
+
const frameworkPaths = new Map([
|
|
144
|
+
...limitedArray(strategy?.files, MAX_GENERATED_FILES).map(file => file?.path),
|
|
145
|
+
...limitedArray(strategy?.cleanupPaths, MAX_MANIFEST_ARRAY_ENTRIES),
|
|
146
|
+
].filter(filename => typeof filename === 'string' && path.isAbsolute(filename))
|
|
147
|
+
.map(filename => [path.normalize(filename), filename]))
|
|
148
|
+
|
|
149
|
+
for (const [key, filename] of frameworkPaths) {
|
|
150
|
+
const previous = seen.get(key)
|
|
151
|
+
if (previous === undefined) {
|
|
152
|
+
seen.set(key, index)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
errors.push(
|
|
156
|
+
`frameworks[${index}].generatedTestStrategy path ${JSON.stringify(filename)} conflicts with ` +
|
|
157
|
+
`frameworks[${previous}].generatedTestStrategy. Generated files and cleanup paths must be unique across ` +
|
|
158
|
+
'framework entries.'
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateDuplicateRunnableCoverage (frameworks, errors) {
|
|
165
|
+
const seen = new Map()
|
|
166
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
167
|
+
if (framework?.status !== 'runnable' || !framework.ciWiringCommand) continue
|
|
168
|
+
const key = JSON.stringify({
|
|
169
|
+
framework: framework.framework,
|
|
170
|
+
projectRoot: framework.project?.root,
|
|
171
|
+
existingTestCommand: commandExecutionShape(framework.existingTestCommand),
|
|
172
|
+
ciWiringCommand: commandExecutionShape(framework.ciWiringCommand),
|
|
173
|
+
ciInitializesDatadog: commandInitializesDatadog(framework.ciWiringCommand),
|
|
174
|
+
})
|
|
175
|
+
const previous = seen.get(key)
|
|
176
|
+
if (previous === undefined) {
|
|
177
|
+
seen.set(key, index)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
errors.push(
|
|
181
|
+
`frameworks[${index}] duplicates runnable framework and CI command coverage from frameworks[${previous}]. ` +
|
|
182
|
+
'Keep one representative framework entry and record the other CI job as an omitted or duplicate candidate.'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function validateUniqueArtifactIds (frameworks, errors) {
|
|
188
|
+
const seen = new Map()
|
|
189
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
190
|
+
if (typeof framework?.id !== 'string') continue
|
|
191
|
+
const artifactId = getArtifactId(framework.id)
|
|
192
|
+
const previous = seen.get(artifactId)
|
|
193
|
+
if (previous !== undefined && previous !== framework.id) {
|
|
194
|
+
errors.push(
|
|
195
|
+
`frameworks[${index}].id collides with another framework artifact identifier after normalization.`
|
|
196
|
+
)
|
|
197
|
+
} else {
|
|
198
|
+
seen.set(artifactId, framework.id)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function commandExecutionShape (command) {
|
|
204
|
+
if (!command) return null
|
|
205
|
+
return {
|
|
206
|
+
argv: command.argv,
|
|
207
|
+
cwd: command.cwd,
|
|
208
|
+
shell: command.shell,
|
|
209
|
+
shellCommand: command.shellCommand,
|
|
210
|
+
usesShell: Boolean(command.usesShell),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function commandInitializesDatadog (command) {
|
|
215
|
+
const values = [
|
|
216
|
+
...(command?.argv || []),
|
|
217
|
+
command?.shellCommand,
|
|
218
|
+
command?.env?.NODE_OPTIONS,
|
|
219
|
+
].filter(Boolean).join(' ')
|
|
220
|
+
return /dd-trace\/ci\/init/.test(values)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateRepositoryContainedPaths (manifest, errors) {
|
|
224
|
+
const repositoryRoot = manifest.repository?.root
|
|
225
|
+
if (typeof repositoryRoot !== 'string' || !path.isAbsolute(repositoryRoot)) return
|
|
226
|
+
|
|
227
|
+
const frameworks = Array.isArray(manifest.frameworks) ? manifest.frameworks.slice(0, MAX_FRAMEWORKS) : []
|
|
228
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
229
|
+
if (!framework || typeof framework !== 'object' || Array.isArray(framework)) continue
|
|
230
|
+
const prefix = `frameworks[${index}]`
|
|
231
|
+
containedPath(repositoryRoot, framework.project?.root, `${prefix}.project.root`, errors)
|
|
232
|
+
containedPath(repositoryRoot, framework.project?.packageJson, `${prefix}.project.packageJson`, errors)
|
|
233
|
+
for (const [configIndex, configFile] of
|
|
234
|
+
limitedArray(framework.project?.configFiles, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
235
|
+
containedPath(repositoryRoot, configFile, `${prefix}.project.configFiles[${configIndex}]`, errors)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const [name, command] of getFrameworkCommands(framework)) {
|
|
239
|
+
containedPath(repositoryRoot, command?.cwd, `${prefix}.${name}.cwd`, errors)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
containedPath(repositoryRoot, framework.ciWiring?.configFile, `${prefix}.ciWiring.configFile`, errors)
|
|
243
|
+
containedPath(repositoryRoot, framework.ciWiring?.workingDirectory, `${prefix}.ciWiring.workingDirectory`, errors)
|
|
244
|
+
|
|
245
|
+
const strategy = framework.generatedTestStrategy
|
|
246
|
+
containedPath(repositoryRoot, strategy?.testDirectory, `${prefix}.generatedTestStrategy.testDirectory`, errors)
|
|
247
|
+
for (const [fileIndex, file] of limitedArray(strategy?.files, MAX_GENERATED_FILES).entries()) {
|
|
248
|
+
containedPath(repositoryRoot, file?.path, `${prefix}.generatedTestStrategy.files[${fileIndex}].path`, errors)
|
|
249
|
+
}
|
|
250
|
+
for (const [cleanupIndex, cleanupPath] of
|
|
251
|
+
limitedArray(strategy?.cleanupPaths, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
252
|
+
containedPath(
|
|
253
|
+
repositoryRoot,
|
|
254
|
+
cleanupPath,
|
|
255
|
+
`${prefix}.generatedTestStrategy.cleanupPaths[${cleanupIndex}]`,
|
|
256
|
+
errors
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
for (const [scenarioIndex, scenario] of
|
|
260
|
+
limitedArray(strategy?.scenarios, GENERATED_SCENARIO_IDS.size).entries()) {
|
|
261
|
+
for (const [identityIndex, identity] of
|
|
262
|
+
limitedArray(scenario?.testIdentities, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
263
|
+
containedPath(
|
|
264
|
+
repositoryRoot,
|
|
265
|
+
identity?.file,
|
|
266
|
+
`${prefix}.generatedTestStrategy.scenarios[${scenarioIndex}].testIdentities[${identityIndex}].file`,
|
|
267
|
+
errors
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getFrameworkCommands (framework) {
|
|
275
|
+
const commands = []
|
|
276
|
+
for (const name of ['existingTestCommand', 'ciWiringCommand']) {
|
|
277
|
+
if (framework[name]) commands.push([name, framework[name]])
|
|
278
|
+
}
|
|
279
|
+
for (const [index, command] of limitedArray(framework.setup?.commands, MAX_SETUP_COMMANDS).entries()) {
|
|
280
|
+
commands.push([`setup.commands[${index}]`, command])
|
|
281
|
+
}
|
|
282
|
+
for (const [index, scenario] of
|
|
283
|
+
limitedArray(framework.generatedTestStrategy?.scenarios, GENERATED_SCENARIO_IDS.size).entries()) {
|
|
284
|
+
if (scenario?.runCommand) {
|
|
285
|
+
commands.push([`generatedTestStrategy.scenarios[${index}].runCommand`, scenario.runCommand])
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return commands
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function containedPath (root, candidate, key, errors) {
|
|
292
|
+
if (typeof candidate !== 'string' || !path.isAbsolute(candidate)) return
|
|
293
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate))
|
|
294
|
+
if (relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))) return
|
|
295
|
+
errors.push(`${key} must be inside repository.root.`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateCiDiscovery (ciDiscovery, prefix, errors) {
|
|
299
|
+
if (!ciDiscovery || typeof ciDiscovery !== 'object' || Array.isArray(ciDiscovery)) {
|
|
300
|
+
errors.push(`${prefix} must be an object when present.`)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const field of ['searched', 'found', 'staticFound', 'warnings', 'notes', 'contradictions']) {
|
|
305
|
+
if (ciDiscovery[field] !== undefined) {
|
|
306
|
+
if (Array.isArray(ciDiscovery[field])) {
|
|
307
|
+
validateStringArray(ciDiscovery, field, errors, prefix)
|
|
308
|
+
} else {
|
|
309
|
+
errors.push(`${prefix}.${field} must be an array when present.`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (ciDiscovery.method !== undefined && typeof ciDiscovery.method !== 'string') {
|
|
315
|
+
errors.push(`${prefix}.method must be a string when present.`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateFramework (framework, index, errors) {
|
|
320
|
+
const prefix = `frameworks[${index}]`
|
|
321
|
+
if (!framework || typeof framework !== 'object' || Array.isArray(framework)) {
|
|
322
|
+
errors.push(`${prefix} must be an object.`)
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
requiredString(framework, 'id', errors, prefix)
|
|
326
|
+
enumString(framework, 'framework', FRAMEWORKS, errors, prefix)
|
|
327
|
+
enumString(framework, 'status', STATUSES, errors, prefix)
|
|
328
|
+
requiredObject(framework, 'project', errors, prefix)
|
|
329
|
+
|
|
330
|
+
if (framework.project) {
|
|
331
|
+
requiredAbsolutePath(framework.project, 'root', errors, `${prefix}.project`)
|
|
332
|
+
optionalAbsolutePath(framework.project, 'packageJson', errors, `${prefix}.project`)
|
|
333
|
+
optionalAbsolutePathArray(framework.project, 'configFiles', errors, `${prefix}.project`)
|
|
334
|
+
validateArrayLimit(framework.project, 'configFiles', MAX_MANIFEST_ARRAY_ENTRIES, errors, `${prefix}.project`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (framework.status === 'runnable') {
|
|
338
|
+
requiredCommand(framework, 'existingTestCommand', errors, prefix, { datadogClean: true })
|
|
339
|
+
validateDatadogCleanCommand(framework.existingTestCommand, `${prefix}.existingTestCommand`, errors)
|
|
340
|
+
requiredObject(framework, 'preflight', errors, prefix)
|
|
341
|
+
validatePreflight(framework.preflight, `${prefix}.preflight`, errors)
|
|
342
|
+
requiredObject(framework, 'ciWiring', errors, prefix)
|
|
343
|
+
} else {
|
|
344
|
+
requireNonEmptyNotes(framework, errors, prefix)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (framework.ciWiringCommand) {
|
|
348
|
+
requiredCommand(framework, 'ciWiringCommand', errors, prefix)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (framework.ciWiring) {
|
|
352
|
+
validateCiWiring(framework, prefix, errors)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (framework.forcedLocalCommand !== undefined) {
|
|
356
|
+
errors.push(
|
|
357
|
+
`${prefix}.forcedLocalCommand is not supported. Use the focused existingTestCommand for Basic Reporting ` +
|
|
358
|
+
'and ciWiringCommand for the CI-shaped replay.'
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (framework.setup?.commands) {
|
|
363
|
+
if (Array.isArray(framework.setup.commands)) {
|
|
364
|
+
validateArrayLimit(framework.setup, 'commands', MAX_SETUP_COMMANDS, errors, `${prefix}.setup`)
|
|
365
|
+
for (const [commandIndex, command] of framework.setup.commands.slice(0, MAX_SETUP_COMMANDS).entries()) {
|
|
366
|
+
requiredCommand({ command }, 'command', errors, `${prefix}.setup.commands[${commandIndex}]`)
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
errors.push(`${prefix}.setup.commands must be an array.`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (framework.generatedTestStrategy) {
|
|
374
|
+
validateGeneratedTestStrategy(framework.generatedTestStrategy, `${prefix}.generatedTestStrategy`, errors)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Validates the approved upper bound for a representative test command.
|
|
380
|
+
*
|
|
381
|
+
* @param {object} preflight preflight declaration
|
|
382
|
+
* @param {string} prefix manifest field path
|
|
383
|
+
* @param {{push: function(string): void}} errors bounded validation error collector
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
function validatePreflight (preflight, prefix, errors) {
|
|
387
|
+
if (!preflight || typeof preflight !== 'object' || Array.isArray(preflight)) return
|
|
388
|
+
|
|
389
|
+
if (!Number.isInteger(preflight.maxTestCount) || preflight.maxTestCount < 1) {
|
|
390
|
+
errors.push(`${prefix}.maxTestCount must be a positive integer.`)
|
|
391
|
+
} else if (preflight.maxTestCount > MAX_REPRESENTATIVE_TESTS) {
|
|
392
|
+
errors.push(`${prefix}.maxTestCount must not exceed ${MAX_REPRESENTATIVE_TESTS}.`)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateGeneratedTestStrategy (strategy, prefix, errors) {
|
|
397
|
+
if (!['planned', 'verified', 'proposed', 'not_possible'].includes(strategy.status)) {
|
|
398
|
+
errors.push(`${prefix}.status must be planned, verified, proposed, or not_possible.`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const completeStrategy = strategy.status === 'planned' || strategy.status === 'verified'
|
|
402
|
+
if (completeStrategy) {
|
|
403
|
+
requiredArray(strategy, 'files', errors, prefix)
|
|
404
|
+
requiredArray(strategy, 'scenarios', errors, prefix)
|
|
405
|
+
requiredArray(strategy, 'cleanupPaths', errors, prefix)
|
|
406
|
+
validateCompleteGeneratedScenarioSet(strategy, prefix, errors)
|
|
407
|
+
} else if ((strategy.status === 'proposed' || strategy.status === 'not_possible') &&
|
|
408
|
+
(typeof strategy.reason !== 'string' || strategy.reason.trim() === '')) {
|
|
409
|
+
errors.push(`${prefix}.reason must explain why the generated test strategy is ${strategy.status}.`)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (Array.isArray(strategy.files)) {
|
|
413
|
+
if (strategy.files.length > MAX_GENERATED_FILES) {
|
|
414
|
+
errors.push(`${prefix}.files must contain at most ${MAX_GENERATED_FILES} generated files.`)
|
|
415
|
+
}
|
|
416
|
+
for (const [index, file] of strategy.files.slice(0, MAX_GENERATED_FILES).entries()) {
|
|
417
|
+
requiredAbsolutePath(file, 'path', errors, `${prefix}.files[${index}]`)
|
|
418
|
+
requiredArray(file, 'contentLines', errors, `${prefix}.files[${index}]`)
|
|
419
|
+
validateStringArray(file, 'contentLines', errors, `${prefix}.files[${index}]`)
|
|
420
|
+
const policyError = getGeneratedFileContentError(file.contentLines)
|
|
421
|
+
if (policyError) errors.push(`${prefix}.files[${index}].contentLines ${policyError}.`)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (Array.isArray(strategy.scenarios)) {
|
|
426
|
+
validateArrayLimit(strategy, 'scenarios', GENERATED_SCENARIO_IDS.size, errors, prefix)
|
|
427
|
+
for (const [index, scenario] of strategy.scenarios.slice(0, GENERATED_SCENARIO_IDS.size).entries()) {
|
|
428
|
+
requiredString(scenario, 'id', errors, `${prefix}.scenarios[${index}]`)
|
|
429
|
+
enumString(scenario, 'id', GENERATED_SCENARIO_IDS, errors, `${prefix}.scenarios[${index}]`)
|
|
430
|
+
requiredCommand(scenario, 'runCommand', errors, `${prefix}.scenarios[${index}]`, { datadogClean: true })
|
|
431
|
+
validateDatadogCleanCommand(scenario.runCommand, `${prefix}.scenarios[${index}].runCommand`, errors)
|
|
432
|
+
validateScenarioIdentities(
|
|
433
|
+
scenario,
|
|
434
|
+
`${prefix}.scenarios[${index}]`,
|
|
435
|
+
errors,
|
|
436
|
+
completeStrategy
|
|
437
|
+
)
|
|
438
|
+
if (completeStrategy) {
|
|
439
|
+
validateGeneratedScenarioOutcome(scenario, `${prefix}.scenarios[${index}]`, errors)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
optionalAbsolutePath(strategy, 'testDirectory', errors, prefix)
|
|
445
|
+
optionalAbsolutePathArray(strategy, 'cleanupPaths', errors, prefix)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validateCiWiring (framework, prefix, errors) {
|
|
449
|
+
const ciWiring = framework.ciWiring
|
|
450
|
+
if (!ciWiring || typeof ciWiring !== 'object' || Array.isArray(ciWiring)) {
|
|
451
|
+
errors.push(`${prefix}.ciWiring must be an object when present.`)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!CI_WIRING_STATUSES.has(ciWiring.status)) {
|
|
456
|
+
errors.push(`${prefix}.ciWiring.status must be pass, fail, skip, or unknown.`)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!CI_WIRING_REPLAYABILITIES.has(ciWiring.replayability)) {
|
|
460
|
+
errors.push(`${prefix}.ciWiring.replayability must be replayable or not_replayable.`)
|
|
461
|
+
}
|
|
462
|
+
if (Object.hasOwn(ciWiring, 'ciWiringCommand')) {
|
|
463
|
+
errors.push(`${prefix}.ciWiring.ciWiringCommand is misplaced; use ${prefix}.ciWiringCommand.`)
|
|
464
|
+
}
|
|
465
|
+
if (ciWiring.replayability === 'replayable' && !framework.ciWiringCommand) {
|
|
466
|
+
errors.push(`${prefix}.ciWiringCommand is required when ${prefix}.ciWiring.replayability is replayable.`)
|
|
467
|
+
}
|
|
468
|
+
if (ciWiring.replayability === 'not_replayable') {
|
|
469
|
+
if (ciWiring.status === 'pass' || ciWiring.status === 'fail') {
|
|
470
|
+
errors.push(`${prefix}.ciWiring.status must be skip or unknown when replayability is not_replayable.`)
|
|
471
|
+
}
|
|
472
|
+
if (framework.ciWiringCommand) {
|
|
473
|
+
errors.push(`${prefix}.ciWiringCommand must be omitted when ${prefix}.ciWiring.replayability is not_replayable.`)
|
|
474
|
+
}
|
|
475
|
+
if (!hasNonEmptyString(ciWiring.replayBlocker)) {
|
|
476
|
+
errors.push(`${prefix}.ciWiring.replayBlocker must explain why CI replay is not_replayable.`)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (ciWiring.initialization !== undefined) {
|
|
481
|
+
validateCiInitialization(ciWiring.initialization, `${prefix}.ciWiring.initialization`, errors)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (ciWiring.initialization?.status === 'not_configured' &&
|
|
485
|
+
commandInitializesDatadog(framework.ciWiringCommand)) {
|
|
486
|
+
errors.push(
|
|
487
|
+
`${prefix}.ciWiring.initialization.status is not_configured, but ${prefix}.ciWiringCommand adds dd-trace ` +
|
|
488
|
+
'initialization. The replay command must preserve the discovered CI configuration; remove the added ' +
|
|
489
|
+
'initialization or correct the initialization status and evidence.'
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (framework.ciWiringCommand) {
|
|
494
|
+
for (const field of ['provider', 'configFile', 'job', 'step', 'whySelected']) {
|
|
495
|
+
requiredString(ciWiring, field, errors, `${prefix}.ciWiring`)
|
|
496
|
+
}
|
|
497
|
+
requiredAbsolutePath(ciWiring, 'configFile', errors, `${prefix}.ciWiring`)
|
|
498
|
+
requiredAbsolutePath(ciWiring, 'workingDirectory', errors, `${prefix}.ciWiring`)
|
|
499
|
+
if (path.resolve(ciWiring.workingDirectory || '') !== path.resolve(framework.ciWiringCommand.cwd || '')) {
|
|
500
|
+
errors.push(`${prefix}.ciWiringCommand.cwd must match ${prefix}.ciWiring.workingDirectory.`)
|
|
501
|
+
}
|
|
502
|
+
if (ciWiring.shell !== undefined && ciWiring.shell !== null) {
|
|
503
|
+
if (typeof ciWiring.shell !== 'string' || ciWiring.shell.trim() === '') {
|
|
504
|
+
errors.push(`${prefix}.ciWiring.shell must be a non-empty string when present.`)
|
|
505
|
+
} else if (hasUnsafeExecutionCharacter(ciWiring.shell)) {
|
|
506
|
+
errors.push(`${prefix}.ciWiring.shell must not contain invisible or control characters.`)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if ((ciWiring.status === 'skip' || ciWiring.status === 'unknown') &&
|
|
512
|
+
!hasNonEmptyString(ciWiring.diagnosis) && !hasNonEmptyString(ciWiring.reason)) {
|
|
513
|
+
errors.push(`${prefix}.ciWiring must explain why CI wiring is ${ciWiring.status}.`)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function validateCiInitialization (initialization, prefix, errors) {
|
|
518
|
+
if (!initialization || typeof initialization !== 'object' || Array.isArray(initialization)) {
|
|
519
|
+
errors.push(`${prefix} must be an object when present.`)
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!CI_INITIALIZATION_STATUSES.has(initialization.status)) {
|
|
524
|
+
errors.push(`${prefix}.status must be configured, not_configured, or unknown.`)
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(initialization.evidence)) {
|
|
527
|
+
validateStringArray(initialization, 'evidence', errors, prefix)
|
|
528
|
+
if (initialization.status !== 'unknown' && initialization.evidence.length === 0) {
|
|
529
|
+
errors.push(`${prefix}.evidence must explain the ${initialization.status} conclusion.`)
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
errors.push(`${prefix}.evidence must be an array.`)
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function hasNonEmptyString (value) {
|
|
537
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function validateDatadogCleanCommand (command, prefix, errors) {
|
|
541
|
+
for (const [name, value] of Object.entries(command?.env || {})) {
|
|
542
|
+
if (name.startsWith('DD_') || (name === 'NODE_OPTIONS' && /dd-trace/.test(String(value)))) {
|
|
543
|
+
errors.push(`${prefix}.env.${name} must not configure Datadog initialization for local validation.`)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const inlineInitialization = getInlineDatadogInitialization(command)
|
|
548
|
+
if (inlineInitialization) {
|
|
549
|
+
errors.push(
|
|
550
|
+
`${prefix} ${inlineInitialization} and must be Datadog-clean for local validation. ` +
|
|
551
|
+
'Remove the inline initialization; preserve exact CI initialization only in ciWiringCommand.'
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function validateGeneratedScenarioOutcome (scenario, prefix, errors) {
|
|
557
|
+
const expected = scenario.expectedWithoutDatadog
|
|
558
|
+
if (!expected || typeof expected !== 'object' || Array.isArray(expected)) {
|
|
559
|
+
errors.push(`${prefix}.expectedWithoutDatadog must be an object when generatedTestStrategy is planned or verified.`)
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const expectedExitCode = GENERATED_SCENARIO_EXIT_CODES[scenario.id]
|
|
564
|
+
if (expectedExitCode !== undefined && expected.exitCode !== expectedExitCode) {
|
|
565
|
+
errors.push(`${prefix}.expectedWithoutDatadog.exitCode must be ${expectedExitCode} for ${scenario.id}.`)
|
|
566
|
+
}
|
|
567
|
+
if (expected.observedTestCount !== 1) {
|
|
568
|
+
errors.push(`${prefix}.expectedWithoutDatadog.observedTestCount must be 1 so the command isolates this scenario.`)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function validateCompleteGeneratedScenarioSet (strategy, prefix, errors) {
|
|
573
|
+
if (!Array.isArray(strategy.scenarios)) return
|
|
574
|
+
|
|
575
|
+
const seen = new Set()
|
|
576
|
+
for (const scenario of strategy.scenarios.slice(0, GENERATED_SCENARIO_IDS.size)) {
|
|
577
|
+
if (typeof scenario?.id === 'string') seen.add(scenario.id)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const scenarioId of GENERATED_SCENARIO_IDS) {
|
|
581
|
+
if (!seen.has(scenarioId)) {
|
|
582
|
+
errors.push(`${prefix}.scenarios must include generated scenario "${scenarioId}" when status is planned or ` +
|
|
583
|
+
'verified.')
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function validateUniqueFrameworkIds (frameworks, errors) {
|
|
589
|
+
const seen = new Set()
|
|
590
|
+
for (const [index, framework] of frameworks.entries()) {
|
|
591
|
+
if (typeof framework?.id !== 'string') continue
|
|
592
|
+
if (seen.has(framework.id)) {
|
|
593
|
+
errors.push(`frameworks[${index}].id must be unique; duplicate "${framework.id}".`)
|
|
594
|
+
}
|
|
595
|
+
seen.add(framework.id)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function requireNonEmptyNotes (framework, errors, prefix) {
|
|
600
|
+
if (!Array.isArray(framework.notes) || framework.notes.length === 0) {
|
|
601
|
+
errors.push(`${prefix}.notes must include a reason when status is ${framework.status}.`)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
validateStringArray(framework, 'notes', errors, prefix)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function validateScenarioIdentities (scenario, prefix, errors, required = false) {
|
|
608
|
+
if (!Array.isArray(scenario.testIdentities)) {
|
|
609
|
+
if (required) {
|
|
610
|
+
errors.push(`${prefix}.testIdentities must be a non-empty array when generatedTestStrategy is planned or ` +
|
|
611
|
+
'verified.')
|
|
612
|
+
}
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (required && scenario.testIdentities.length === 0) {
|
|
617
|
+
errors.push(`${prefix}.testIdentities must be a non-empty array when generatedTestStrategy is planned or verified.`)
|
|
618
|
+
}
|
|
619
|
+
validateArrayLimit(
|
|
620
|
+
{ testIdentities: scenario.testIdentities },
|
|
621
|
+
'testIdentities',
|
|
622
|
+
MAX_MANIFEST_ARRAY_ENTRIES,
|
|
623
|
+
errors,
|
|
624
|
+
prefix
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
for (const [index, identity] of scenario.testIdentities.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
628
|
+
const identityPrefix = `${prefix}.testIdentities[${index}]`
|
|
629
|
+
if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
|
|
630
|
+
errors.push(`${identityPrefix} must be an object.`)
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
requiredString(identity, 'name', errors, identityPrefix)
|
|
634
|
+
if (identity.suite !== undefined && identity.suite !== null && typeof identity.suite !== 'string') {
|
|
635
|
+
errors.push(`${identityPrefix}.suite must be a string or null when present.`)
|
|
636
|
+
}
|
|
637
|
+
optionalAbsolutePath(identity, 'file', errors, identityPrefix)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function requiredCommand (target, field, errors, prefix = '', options = {}) {
|
|
642
|
+
const value = target && target[field]
|
|
643
|
+
const key = join(prefix, field)
|
|
644
|
+
if (!value || typeof value !== 'object') {
|
|
645
|
+
errors.push(`${key} must be an object.`)
|
|
646
|
+
return
|
|
647
|
+
}
|
|
648
|
+
for (const name of Object.keys(value)) {
|
|
649
|
+
if (!COMMAND_FIELDS.has(name)) errors.push(`${key}.${name} is not an allowed command field.`)
|
|
650
|
+
}
|
|
651
|
+
if (value.usesShell !== undefined && typeof value.usesShell !== 'boolean') {
|
|
652
|
+
errors.push(`${key}.usesShell must be a boolean when present.`)
|
|
653
|
+
}
|
|
654
|
+
if (value.required !== undefined && typeof value.required !== 'boolean') {
|
|
655
|
+
errors.push(`${key}.required must be a boolean when present.`)
|
|
656
|
+
}
|
|
657
|
+
requiredAbsolutePath(value, 'cwd', errors, key)
|
|
658
|
+
rejectUnresolvedPlaceholder(value.cwd, `${key}.cwd`, errors)
|
|
659
|
+
if (value.shell !== undefined) {
|
|
660
|
+
requiredString(value, 'shell', errors, key)
|
|
661
|
+
rejectUnresolvedPlaceholder(value.shell, `${key}.shell`, errors)
|
|
662
|
+
if (!value.usesShell) errors.push(`${key}.shell requires usesShell to be true.`)
|
|
663
|
+
if (typeof value.shell === 'string' && hasUnsafeExecutionCharacter(value.shell)) {
|
|
664
|
+
errors.push(`${key}.shell must not contain invisible or control characters.`)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (value.usesShell) {
|
|
668
|
+
requiredString(value, 'shellCommand', errors, key)
|
|
669
|
+
rejectUnresolvedPlaceholder(value.shellCommand, `${key}.shellCommand`, errors)
|
|
670
|
+
if (typeof value.shellCommand === 'string' && hasUnsafeInvisibleCharacter(value.shellCommand)) {
|
|
671
|
+
errors.push(`${key}.shellCommand must not contain invisible or control characters.`)
|
|
672
|
+
} else if (typeof value.shellCommand === 'string' && sanitizeString(value.shellCommand) !== value.shellCommand) {
|
|
673
|
+
errors.push(`${key}.shellCommand must not contain inline secret-like values. Put safe placeholders in env.`)
|
|
674
|
+
}
|
|
675
|
+
} else if (!Array.isArray(value.argv) || value.argv.length === 0) {
|
|
676
|
+
errors.push(`${key}.argv must be a non-empty array unless usesShell is true.`)
|
|
677
|
+
} else {
|
|
678
|
+
validateStringArray(value, 'argv', errors, key)
|
|
679
|
+
for (const [index, arg] of value.argv.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
680
|
+
rejectUnresolvedPlaceholder(arg, `${key}.argv[${index}]`, errors)
|
|
681
|
+
}
|
|
682
|
+
if (value.argv.some(hasUnsafeExecutionCharacter)) {
|
|
683
|
+
errors.push(`${key}.argv must not contain invisible or control characters.`)
|
|
684
|
+
} else if (JSON.stringify(sanitizeForReport(value.argv)) !== JSON.stringify(value.argv)) {
|
|
685
|
+
errors.push(`${key}.argv must not contain inline secret-like values. Put safe placeholders in env.`)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (value.env !== undefined) {
|
|
689
|
+
if (!value.env || typeof value.env !== 'object' || Array.isArray(value.env)) {
|
|
690
|
+
errors.push(`${key}.env must be an object when present.`)
|
|
691
|
+
} else {
|
|
692
|
+
const environmentEntries = Object.entries(value.env)
|
|
693
|
+
if (environmentEntries.length > MAX_MANIFEST_ARRAY_ENTRIES) {
|
|
694
|
+
errors.push(`${key}.env must contain at most ${MAX_MANIFEST_ARRAY_ENTRIES} entries.`)
|
|
695
|
+
}
|
|
696
|
+
for (const [name, envValue] of environmentEntries.slice(0, MAX_MANIFEST_ARRAY_ENTRIES)) {
|
|
697
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
698
|
+
errors.push(`${key}.env contains invalid variable name ${JSON.stringify(name)}.`)
|
|
699
|
+
}
|
|
700
|
+
if (typeof envValue !== 'string') errors.push(`${key}.env.${name} must be a string.`)
|
|
701
|
+
const validatePlaceholder = !(options.datadogClean && name.startsWith('DD_'))
|
|
702
|
+
if (typeof envValue === 'string' && hasUnsafeExecutionCharacter(envValue)) {
|
|
703
|
+
errors.push(`${key}.env.${name} must not contain invisible or control characters.`)
|
|
704
|
+
} else if (validatePlaceholder && typeof envValue === 'string' && containsSecretValue(name, envValue) &&
|
|
705
|
+
envValue !== SECRET_PLACEHOLDER) {
|
|
706
|
+
errors.push(`${key}.env.${name} must use the safe placeholder ${JSON.stringify(SECRET_PLACEHOLDER)}.`)
|
|
707
|
+
}
|
|
708
|
+
rejectUnresolvedPlaceholder(envValue, `${key}.env.${name}`, errors)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (value.requiredEnvVars !== undefined) {
|
|
713
|
+
if (Array.isArray(value.requiredEnvVars)) {
|
|
714
|
+
validateArrayLimit(value, 'requiredEnvVars', MAX_MANIFEST_ARRAY_ENTRIES, errors, key)
|
|
715
|
+
for (const [index] of value.requiredEnvVars.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
716
|
+
requiredString(value.requiredEnvVars, index, errors, `${key}.requiredEnvVars`)
|
|
717
|
+
}
|
|
718
|
+
} else {
|
|
719
|
+
errors.push(`${key}.requiredEnvVars must be an array when present.`)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
optionalAbsolutePathArray(value, 'outputPaths', errors, key)
|
|
723
|
+
if (value.timeoutMs !== undefined && (!Number.isFinite(value.timeoutMs) || value.timeoutMs <= 0)) {
|
|
724
|
+
errors.push(`${key}.timeoutMs must be a positive number when present.`)
|
|
725
|
+
} else if (value.timeoutMs > MAX_COMMAND_TIMEOUT_MS) {
|
|
726
|
+
errors.push(`${key}.timeoutMs must not exceed ${MAX_COMMAND_TIMEOUT_MS} ms.`)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function containsSecretValue (name, value) {
|
|
731
|
+
if (SAFE_SECRET_FIELD_VALUES.has(value.toLowerCase())) return false
|
|
732
|
+
return isSensitiveName(name) || sanitizeString(value) !== value
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function rejectUnresolvedPlaceholder (value, key, errors) {
|
|
736
|
+
if (typeof value !== 'string' || !UNRESOLVED_PLACEHOLDER_PATTERN.test(value)) return
|
|
737
|
+
errors.push(`${key} contains an unresolved placeholder. Resolve it before live validation.`)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function requiredObject (target, field, errors, prefix = '') {
|
|
741
|
+
const value = target && target[field]
|
|
742
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
743
|
+
errors.push(`${join(prefix, field)} must be an object.`)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function requiredArray (target, field, errors, prefix = '') {
|
|
748
|
+
if (!Array.isArray(target && target[field])) {
|
|
749
|
+
errors.push(`${join(prefix, field)} must be an array.`)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function requiredString (target, field, errors, prefix = '') {
|
|
754
|
+
if (typeof (target && target[field]) !== 'string' || target[field].length === 0) {
|
|
755
|
+
errors.push(`${join(prefix, field)} must be a non-empty string.`)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function enumString (target, field, values, errors, prefix = '') {
|
|
760
|
+
if (!values.has(target && target[field])) {
|
|
761
|
+
errors.push(`${join(prefix, field)} must be one of: ${[...values].join(', ')}.`)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function requiredAbsolutePath (target, field, errors, prefix = '') {
|
|
766
|
+
const value = target && target[field]
|
|
767
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) {
|
|
768
|
+
errors.push(`${join(prefix, field)} must be an absolute path.`)
|
|
769
|
+
} else if (hasUnsafeExecutionCharacter(value)) {
|
|
770
|
+
errors.push(`${join(prefix, field)} must not contain invisible or control characters.`)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function optionalAbsolutePath (target, field, errors, prefix = '') {
|
|
775
|
+
const value = target && target[field]
|
|
776
|
+
if (value === undefined || value === null) return
|
|
777
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) {
|
|
778
|
+
errors.push(`${join(prefix, field)} must be an absolute path when present.`)
|
|
779
|
+
} else if (hasUnsafeExecutionCharacter(value)) {
|
|
780
|
+
errors.push(`${join(prefix, field)} must not contain invisible or control characters.`)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function optionalAbsolutePathArray (target, field, errors, prefix = '') {
|
|
785
|
+
const value = target && target[field]
|
|
786
|
+
if (value === undefined) return
|
|
787
|
+
if (!Array.isArray(value)) {
|
|
788
|
+
errors.push(`${join(prefix, field)} must be an array when present.`)
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
validateArrayLimit(target, field, MAX_MANIFEST_ARRAY_ENTRIES, errors, prefix)
|
|
793
|
+
for (const [index, item] of value.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
794
|
+
if (typeof item !== 'string' || !path.isAbsolute(item)) {
|
|
795
|
+
errors.push(`${join(prefix, field)}[${index}] must be an absolute path.`)
|
|
796
|
+
} else if (hasUnsafeExecutionCharacter(item)) {
|
|
797
|
+
errors.push(`${join(prefix, field)}[${index}] must not contain invisible or control characters.`)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function validateStringArray (target, field, errors, prefix = '') {
|
|
803
|
+
const value = target && target[field]
|
|
804
|
+
if (!Array.isArray(value)) return
|
|
805
|
+
|
|
806
|
+
validateArrayLimit(target, field, MAX_MANIFEST_ARRAY_ENTRIES, errors, prefix)
|
|
807
|
+
for (const [index, item] of value.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
|
|
808
|
+
if (typeof item !== 'string') {
|
|
809
|
+
errors.push(`${join(prefix, field)}[${index}] must be a string.`)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function validateArrayLimit (target, field, limit, errors, prefix = '') {
|
|
815
|
+
const value = target && target[field]
|
|
816
|
+
if (Array.isArray(value) && value.length > limit) {
|
|
817
|
+
errors.push(`${join(prefix, field)} must contain at most ${limit} entries.`)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function limitedArray (value, limit) {
|
|
822
|
+
return Array.isArray(value) ? value.slice(0, limit) : []
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function createErrorCollector () {
|
|
826
|
+
const errors = []
|
|
827
|
+
let omitted = 0
|
|
828
|
+
Object.defineProperties(errors, {
|
|
829
|
+
push: {
|
|
830
|
+
value (...messages) {
|
|
831
|
+
for (const message of messages) {
|
|
832
|
+
if (this.length < MAX_VALIDATION_ERRORS - 1) {
|
|
833
|
+
Array.prototype.push.call(this, message)
|
|
834
|
+
} else {
|
|
835
|
+
omitted++
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return this.length
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
finalize: {
|
|
842
|
+
value () {
|
|
843
|
+
if (omitted > 0) {
|
|
844
|
+
Array.prototype.push.call(this, `${omitted} additional validation error(s) omitted.`)
|
|
845
|
+
}
|
|
846
|
+
return this
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
})
|
|
850
|
+
return errors
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function join (prefix, field) {
|
|
854
|
+
return prefix ? `${prefix}.${field}` : field
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
module.exports = {
|
|
858
|
+
MAX_FRAMEWORKS,
|
|
859
|
+
MAX_REPRESENTATIVE_TESTS,
|
|
860
|
+
MAX_VALIDATION_ERRORS,
|
|
861
|
+
validateManifest,
|
|
862
|
+
}
|