dd-trace 5.116.0 → 5.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +3 -3
  45. package/packages/datadog-instrumentations/src/cucumber.js +11 -12
  46. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  47. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  48. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  49. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  50. package/packages/datadog-instrumentations/src/jest.js +18 -17
  51. package/packages/datadog-instrumentations/src/mocha/main.js +79 -30
  52. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  53. package/packages/datadog-instrumentations/src/playwright.js +3 -24
  54. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -1
  55. package/packages/datadog-instrumentations/src/vitest-main.js +14 -26
  56. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  57. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  58. package/packages/datadog-plugin-cucumber/src/index.js +2 -1
  59. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  60. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  61. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  62. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  63. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  64. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  65. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  66. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  67. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  68. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  69. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  70. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  71. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  72. package/packages/dd-trace/src/exporter.js +16 -9
  73. package/packages/dd-trace/src/llmobs/index.js +9 -1
  74. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  75. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  76. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  77. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  78. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  79. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  80. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  81. package/packages/dd-trace/src/llmobs/util.js +56 -3
  82. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  83. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  84. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  85. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  86. package/packages/dd-trace/src/plugins/index.js +1 -0
  87. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  88. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  89. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  90. package/packages/dd-trace/src/span_processor.js +1 -1
  91. package/packages/dd-trace/src/span_stats.js +22 -4
  92. package/ci/test-optimization-validation/command-suitability.js +0 -471
  93. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  94. package/ci/test-optimization-validation/init-probe.js +0 -246
  95. package/ci/test-optimization-validation/late-initialization.js +0 -63
  96. package/ci/test-optimization-validation/local-command.js +0 -163
  97. package/ci/test-optimization-validation/setup-runner.js +0 -97
  98. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -0,0 +1,283 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+
5
+ const cucumberAdapter = require('./framework-adapters/cucumber')
6
+ const cypressAdapter = require('./framework-adapters/cypress')
7
+ const playwrightAdapter = require('./framework-adapters/playwright')
8
+
9
+ const GENERATED_SCENARIOS = {
10
+ 'basic-pass': {
11
+ expectedWithoutDatadog: { exitCode: 0, observedTestCount: 1 },
12
+ purpose: 'basic_reporting|efd_candidate',
13
+ suiteName: 'dd-test-optimization-validation',
14
+ testName: 'basic-pass',
15
+ },
16
+ 'atr-fail-once': {
17
+ expectedWithoutDatadog: { exitCode: 1, observedTestCount: 1 },
18
+ purpose: 'auto_test_retries_candidate',
19
+ suiteName: 'dd-test-optimization-validation',
20
+ testName: 'atr-fail-once',
21
+ },
22
+ 'test-management-target': {
23
+ expectedWithoutDatadog: { exitCode: 0, observedTestCount: 1 },
24
+ purpose: 'test_management_candidate',
25
+ suiteName: 'dd-test-optimization-validation',
26
+ testName: 'test-management-target',
27
+ },
28
+ }
29
+
30
+ /**
31
+ * Returns the retry-state path used by generated Node.js test scenarios.
32
+ *
33
+ * @param {string} framework test framework
34
+ * @param {string} filename generated ATR test file
35
+ * @returns {string} retry-state path
36
+ */
37
+ function getGeneratedRetryStatePath (framework, filename) {
38
+ const namespace = `dd-test-optimization-validation-${framework}-`
39
+ const namespaced = path.basename(filename).includes(namespace) ||
40
+ path.basename(path.dirname(filename)).includes(namespace)
41
+ const stateFilename = namespaced
42
+ ? `.dd-test-optimization-validation-${framework}-atr-state`
43
+ : '.dd-test-optimization-validation-atr-state'
44
+ return path.join(path.dirname(filename), stateFilename)
45
+ }
46
+
47
+ /**
48
+ * Returns validator-owned source for one generated scenario.
49
+ *
50
+ * @param {object} input generated source input
51
+ * @param {string} input.framework test framework
52
+ * @param {string} input.moduleSystem generated module system
53
+ * @param {string} input.scenarioId generated scenario id
54
+ * @param {string} [input.stateFile] persistent ATR state file
55
+ * @returns {string} canonical generated source
56
+ */
57
+ function getGeneratedTestContent ({ framework, moduleSystem, scenarioId, stateFile }) {
58
+ const scenario = GENERATED_SCENARIOS[scenarioId]
59
+ if (!scenario) throw new Error(`Unknown generated Test Optimization scenario: ${scenarioId}`)
60
+ if (framework === 'cucumber') {
61
+ return cucumberAdapter.getGeneratedTestContent({ testName: scenario.testName })
62
+ }
63
+ if (framework === 'cypress') {
64
+ return cypressAdapter.getGeneratedTestContent({
65
+ scenarioId,
66
+ testName: scenario.testName,
67
+ })
68
+ }
69
+ if (framework === 'playwright') {
70
+ return playwrightAdapter.getGeneratedTestContent({
71
+ moduleSystem,
72
+ scenarioId,
73
+ testName: scenario.testName,
74
+ })
75
+ }
76
+
77
+ const imports = []
78
+ if (framework === 'jest') {
79
+ const requireCall = 'require'
80
+ imports.push(moduleSystem === 'esm'
81
+ ? "import { describe, expect, test } from '@jest/globals'"
82
+ : `const { describe, expect, test } = ${requireCall}('@jest/globals')`)
83
+ }
84
+ if (framework === 'vitest' && moduleSystem === 'esm') {
85
+ imports.push("import { describe, expect, it } from 'vitest'")
86
+ }
87
+ if (framework === 'mocha') {
88
+ imports.push(moduleSystem === 'esm'
89
+ ? "import assert from 'node:assert/strict'"
90
+ : "const assert = require('node:assert/strict')")
91
+ }
92
+ if (scenarioId === 'atr-fail-once') {
93
+ if (moduleSystem === 'esm') {
94
+ imports.push("import { existsSync, writeFileSync } from 'node:fs'")
95
+ } else {
96
+ imports.push("const fs = require('node:fs')")
97
+ }
98
+ }
99
+
100
+ const assertion = framework === 'mocha' ? 'assert.equal(1, 1)' : 'expect(true).toBe(true)'
101
+ const testFunction = framework === 'jest' ? 'test' : 'it'
102
+ const body = scenarioId === 'atr-fail-once'
103
+ ? getAtrBody({ moduleSystem, assertion, stateFile })
104
+ : ` ${assertion}`
105
+
106
+ return [
107
+ ...imports,
108
+ imports.length > 0 ? '' : undefined,
109
+ "describe('dd-test-optimization-validation', () => {",
110
+ ` ${testFunction}('${scenario.testName}', () => {`,
111
+ body,
112
+ ' })',
113
+ '})',
114
+ ].filter(line => line !== undefined).join('\n')
115
+ }
116
+
117
+ /**
118
+ * Verifies that a runnable framework retained the validator-owned generated-test recipe.
119
+ *
120
+ * @param {object} framework manifest framework entry
121
+ * @returns {string|undefined} contract error
122
+ */
123
+ function getGeneratedTestContractError (framework) {
124
+ const strategy = framework.generatedTestStrategy
125
+ if (!strategy || !['planned', 'verified'].includes(strategy.status)) return
126
+
127
+ if (!['cucumber', 'cypress', 'jest', 'mocha', 'playwright', 'vitest'].includes(framework.framework)) return
128
+ if (!['commonjs', 'esm'].includes(strategy.moduleSystem)) {
129
+ return 'must retain generatedTestStrategy.moduleSystem as "commonjs" or "esm".'
130
+ }
131
+
132
+ const files = strategy.files || []
133
+ const scenarios = strategy.scenarios || []
134
+ const projectRoot = framework.project?.root
135
+ if (typeof projectRoot !== 'string') return 'must declare a project root for generated tests.'
136
+ if ((strategy.cleanupPaths || []).some(filename => typeof filename !== 'string')) {
137
+ return 'must contain only string cleanup paths.'
138
+ }
139
+ const cleanupPaths = new Set((strategy.cleanupPaths || []).map(filename => path.normalize(filename)))
140
+ const expectedFileCount = ['cucumber', 'playwright'].includes(framework.framework) ? 4 : 3
141
+ if (files.length !== expectedFileCount || scenarios.length !== 3) {
142
+ return 'must contain exactly one validator-owned file for each of basic-pass, atr-fail-once, and ' +
143
+ `test-management-target${getAdditionalGeneratedFileDescription(framework.framework)}.`
144
+ }
145
+
146
+ const selectedFiles = new Set()
147
+ for (const [scenarioId, definition] of Object.entries(GENERATED_SCENARIOS)) {
148
+ const scenario = scenarios.find(entry => entry.id === scenarioId)
149
+ if (!scenario) return `is missing the validator-owned ${scenarioId} scenario.`
150
+ if (scenario.testIdentities?.length !== 1) {
151
+ return `scenario ${scenarioId} must declare exactly one generated test identity.`
152
+ }
153
+
154
+ const filename = scenario.testIdentities[0].file
155
+ if (typeof filename !== 'string') return `scenario ${scenarioId} must identify a generated test file.`
156
+ if (!isPathInside(projectRoot, filename)) {
157
+ return `scenario ${scenarioId} file must remain inside project root ${projectRoot}.`
158
+ }
159
+ const file = files.find(entry => {
160
+ return typeof entry.path === 'string' && path.normalize(entry.path) === path.normalize(filename)
161
+ })
162
+ if (!file) return `scenario ${scenarioId} must identify exactly one file in generatedTestStrategy.files.`
163
+ selectedFiles.add(path.normalize(filename))
164
+ if (scenario.testIdentities[0].name !== definition.testName) {
165
+ return `scenario ${scenarioId} must retain test name ${JSON.stringify(definition.testName)}.`
166
+ }
167
+
168
+ const stateFile = ['cucumber', 'cypress', 'playwright'].includes(framework.framework)
169
+ ? undefined
170
+ : getGeneratedRetryStatePath(framework.framework, filename)
171
+ const expectedSource = getGeneratedTestContent({
172
+ framework: framework.framework,
173
+ moduleSystem: strategy.moduleSystem,
174
+ scenarioId,
175
+ stateFile,
176
+ })
177
+ if (file.contentLines?.join('\n') !== expectedSource) {
178
+ return `scenario ${scenarioId} source differs from the validator-owned ${framework.framework} recipe. ` +
179
+ 'Regenerate the manifest scaffold instead of rewriting temporary tests.'
180
+ }
181
+ if (!cleanupPaths.has(path.normalize(filename))) {
182
+ return `scenario ${scenarioId} file must be included in generatedTestStrategy.cleanupPaths.`
183
+ }
184
+ }
185
+
186
+ if (framework.framework === 'cucumber') {
187
+ const stepsFile = cucumberAdapter.getGeneratedStepsPath(strategy.testDirectory)
188
+ const steps = files.find(file => path.normalize(file.path) === path.normalize(stepsFile))
189
+ if (steps?.contentLines?.join('\n') !== cucumberAdapter.getGeneratedStepsContent()) {
190
+ return 'must retain the validator-owned Cucumber step definitions.'
191
+ }
192
+ if (!cleanupPaths.has(path.normalize(stepsFile))) {
193
+ return 'must include the isolated Cucumber step definitions in cleanupPaths.'
194
+ }
195
+ }
196
+
197
+ if (framework.framework === 'playwright') {
198
+ const configPath = playwrightAdapter.getGeneratedConfigPath(strategy.testDirectory)
199
+ const config = files.find(file => path.normalize(file.path) === path.normalize(configPath))
200
+ if (config?.contentLines?.join('\n') !== playwrightAdapter.getGeneratedConfigContent()) {
201
+ return 'must retain the validator-owned isolated Playwright config.'
202
+ }
203
+ if (!cleanupPaths.has(path.normalize(configPath))) {
204
+ return 'must include the isolated Playwright config in generatedTestStrategy.cleanupPaths.'
205
+ }
206
+ }
207
+
208
+ const usesPersistentRetryState = !['cucumber', 'cypress', 'playwright'].includes(framework.framework)
209
+ if (usesPersistentRetryState) {
210
+ const atrScenario = scenarios.find(entry => entry.id === 'atr-fail-once')
211
+ const stateFile = getGeneratedRetryStatePath(framework.framework, atrScenario.testIdentities[0].file)
212
+ if (!cleanupPaths.has(path.normalize(stateFile))) {
213
+ return `scenario atr-fail-once must clean up its persistent retry state file ${stateFile}.`
214
+ }
215
+ }
216
+ const expectedCleanupPathCount = usesPersistentRetryState ? 4 : expectedFileCount
217
+ if (selectedFiles.size !== 3 || cleanupPaths.size !== expectedCleanupPathCount) {
218
+ if (framework.framework === 'cucumber') {
219
+ return 'must use three distinct generated feature files and clean up those files plus the isolated Cucumber ' +
220
+ 'step definitions.'
221
+ }
222
+ return usesPersistentRetryState
223
+ ? 'must use three distinct generated test files and clean up exactly those files plus the persistent ATR ' +
224
+ 'state file.'
225
+ : 'must use three distinct generated test files and clean up exactly those files.'
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Describes adapter-specific files in generated-contract errors.
231
+ *
232
+ * @param {string} framework framework name
233
+ * @returns {string} additional file description
234
+ */
235
+ function getAdditionalGeneratedFileDescription (framework) {
236
+ if (framework === 'cucumber') return ' plus isolated Cucumber step definitions'
237
+ if (framework === 'playwright') return ' plus one isolated Playwright config'
238
+ return ''
239
+ }
240
+
241
+ /**
242
+ * Checks whether a generated path is inside or equal to the declared project root.
243
+ *
244
+ * @param {string} root project root
245
+ * @param {string} filename generated path
246
+ * @returns {boolean} whether the path is contained
247
+ */
248
+ function isPathInside (root, filename) {
249
+ const relative = path.relative(path.resolve(root), path.resolve(filename))
250
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
251
+ }
252
+
253
+ /**
254
+ * Returns the persistent retry body used across independent test-runner processes.
255
+ *
256
+ * @param {object} input body input
257
+ * @param {string} input.moduleSystem generated module system
258
+ * @param {string} input.assertion passing assertion
259
+ * @param {string} input.stateFile persistent ATR state file
260
+ * @returns {string} retry test body
261
+ */
262
+ function getAtrBody ({ moduleSystem, assertion, stateFile }) {
263
+ if (typeof stateFile !== 'string' || !path.isAbsolute(stateFile)) {
264
+ throw new Error('The validator-owned ATR recipe requires an absolute persistent state file path.')
265
+ }
266
+ const exists = moduleSystem === 'esm' ? 'existsSync' : 'fs.existsSync'
267
+ const write = moduleSystem === 'esm' ? 'writeFileSync' : 'fs.writeFileSync'
268
+ return [
269
+ ` const stateFile = ${JSON.stringify(stateFile)}`,
270
+ ` if (!${exists}(stateFile)) {`,
271
+ ` ${write}(stateFile, 'failed-once')`,
272
+ " throw new Error('dd-test-optimization-validation atr first failure')",
273
+ ' }',
274
+ ` ${assertion}`,
275
+ ].join('\n')
276
+ }
277
+
278
+ module.exports = {
279
+ GENERATED_SCENARIOS,
280
+ getGeneratedRetryStatePath,
281
+ getGeneratedTestContent,
282
+ getGeneratedTestContractError,
283
+ }
@@ -3,15 +3,13 @@
3
3
  const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
 
6
+ const { getCommandBlocker } = require('./command-blocker')
6
7
  const { runCommand, serializeDisplayCommand } = require('./command-runner')
7
8
  const {
8
9
  cleanupGeneratedRuntimeFiles,
9
10
  writeGeneratedFiles,
10
11
  } = require('./generated-files')
11
- const {
12
- getDatadogCleanCommand,
13
- getLocalValidationCommand,
14
- } = require('./local-command')
12
+ const { getGeneratedCommand } = require('./runner-command')
15
13
  const { frameworkOutDir } = require('./scenarios/helpers')
16
14
  const { getObservedTestCount } = require('./test-output')
17
15
 
@@ -42,11 +40,11 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
42
40
 
43
41
  try {
44
42
  cleanupGeneratedRuntimeFiles(framework)
45
- writeGeneratedFiles(framework)
46
43
 
47
44
  for (const scenario of getScenariosToVerify(strategy.scenarios, options.scenarios)) {
48
45
  cleanupGeneratedRuntimeFiles(framework)
49
- const command = getDatadogCleanCommand(getLocalValidationCommand(framework, scenario.runCommand))
46
+ writeGeneratedFiles(framework, scenario)
47
+ const command = getGeneratedCommand(framework, scenario)
50
48
  const outDir = frameworkOutDir(out, framework, `generated-verification-${scenario.id}`)
51
49
  // Generated commands run serially because fail-once state and cleanup are scenario-local.
52
50
  // eslint-disable-next-line no-await-in-loop
@@ -62,7 +60,7 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
62
60
  const observedTestCount = getObservedTestCount(framework.framework, result.stdout, result.stderr)
63
61
  const expected = scenario.expectedWithoutDatadog
64
62
  const failOnceStateCreated = scenario.id === 'atr-fail-once'
65
- ? hasGeneratedRuntimeFile(strategy)
63
+ ? getGeneratedRuntimeFileStatus(strategy)
66
64
  : undefined
67
65
  const scenarioEvidence = {
68
66
  id: scenario.id,
@@ -77,10 +75,19 @@ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
77
75
  evidence.scenarios.push(scenarioEvidence)
78
76
  artifacts.push(...Object.values(result.artifacts))
79
77
 
78
+ const observedWrongTestCount = observedTestCount !== null &&
79
+ observedTestCount !== expected.observedTestCount
80
80
  if (result.timedOut || result.exitCode !== expected.exitCode ||
81
- observedTestCount !== expected.observedTestCount || failOnceStateCreated === false) {
81
+ observedWrongTestCount || failOnceStateCreated === false) {
82
82
  cleanupGeneratedRuntimeFiles(framework)
83
- return getVerificationFailure(framework, evidence, artifacts, scenarioEvidence, result.timedOut)
83
+ return getVerificationFailure(
84
+ framework,
85
+ evidence,
86
+ artifacts,
87
+ scenarioEvidence,
88
+ result,
89
+ observedTestCount
90
+ )
84
91
  }
85
92
  }
86
93
 
@@ -138,12 +145,36 @@ function getScenariosToVerify (scenarios, selectedFeatures) {
138
145
  * @param {object} evidence collected verification evidence
139
146
  * @param {string[]} artifacts command artifacts
140
147
  * @param {object} scenario failed scenario evidence
141
- * @param {boolean} timedOut whether the scenario timed out
148
+ * @param {object} result command result
149
+ * @param {number|null} observedTestCount observed generated test count
142
150
  * @returns {{ok: false, failure: object}} generated verification failure
143
151
  */
144
- function getVerificationFailure (framework, evidence, artifacts, scenario, timedOut) {
152
+ function getVerificationFailure (framework, evidence, artifacts, scenario, result, observedTestCount) {
153
+ const commandFailure = getCommandBlocker(result, {
154
+ browserRequired: framework.browserRequired,
155
+ framework: framework.framework,
156
+ testsRan: Number.isInteger(observedTestCount) && observedTestCount > 0,
157
+ })
158
+ if (commandFailure) {
159
+ const domain = commandFailure.blockedByExecutionEnvironment
160
+ ? 'execution_environment'
161
+ : commandFailure.localRuntimeBlocked
162
+ ? 'local_runtime'
163
+ : 'project_setup'
164
+ return {
165
+ ok: false,
166
+ failure: {
167
+ frameworkId: framework.id,
168
+ scenario: 'generated-test-verification',
169
+ status: 'blocked',
170
+ diagnosis: `${commandFailure.summary} Advanced-feature validation could not start reliably.`,
171
+ evidence: { ...evidence, commandFailure, domain, validationIncomplete: true },
172
+ artifacts,
173
+ },
174
+ }
175
+ }
145
176
  let reason
146
- if (timedOut) {
177
+ if (result.timedOut) {
147
178
  reason = 'timed out'
148
179
  } else if (scenario.id === 'atr-fail-once' && scenario.failOnceStateCreated === false) {
149
180
  reason = 'failed without creating its declared fail-once state file, so it failed for an unrelated reason'
@@ -165,22 +196,24 @@ function getVerificationFailure (framework, evidence, artifacts, scenario, timed
165
196
  }
166
197
 
167
198
  /**
168
- * Checks whether the generated fail-once scenario created a declared runtime state file.
199
+ * Checks a declared generated runtime state file when the adapter uses one.
169
200
  *
170
201
  * @param {object} strategy generated test strategy
171
- * @returns {boolean} whether a regular declared runtime file exists
202
+ * @returns {boolean|undefined} whether a declared runtime file exists, or undefined when none is required
172
203
  */
173
- function hasGeneratedRuntimeFile (strategy) {
204
+ function getGeneratedRuntimeFileStatus (strategy) {
174
205
  const generatedFiles = new Set((strategy.files || []).map(file => path.resolve(file.path)))
206
+ let expectsRuntimeFile = false
175
207
  for (const cleanupPath of strategy.cleanupPaths || []) {
176
208
  const filename = path.resolve(cleanupPath)
177
209
  if (generatedFiles.has(filename)) continue
210
+ expectsRuntimeFile = true
178
211
  try {
179
212
  const stat = fs.lstatSync(filename)
180
213
  if (!stat.isSymbolicLink() && stat.isFile()) return true
181
214
  } catch {}
182
215
  }
183
- return false
216
+ return expectsRuntimeFile ? false : undefined
184
217
  }
185
218
 
186
219
  /**
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { environmentNamesEqual } = require('./environment')
4
+
5
+ const ASSIGNMENT_PATTERN =
6
+ /([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s]*))(?:[ \t]+|$)/y
7
+
8
+ /**
9
+ * Parses only complete literal environment assignments at the start of a command.
10
+ *
11
+ * @param {string} command command text
12
+ * @returns {{assignments: Array<{name: string, source: string, value: string}>, length: number}} parsed prefix
13
+ */
14
+ function parseLiteralEnvironmentPrefix (command) {
15
+ const source = String(command || '')
16
+ const assignments = []
17
+ let offset = 0
18
+
19
+ while (offset < source.length) {
20
+ ASSIGNMENT_PATTERN.lastIndex = offset
21
+ const match = ASSIGNMENT_PATTERN.exec(source)
22
+ if (!match) break
23
+ assignments.push({
24
+ name: match[1],
25
+ source: match[0].trimEnd(),
26
+ value: match[2] ?? match[3] ?? match[4],
27
+ })
28
+ offset = ASSIGNMENT_PATTERN.lastIndex
29
+ }
30
+
31
+ return { assignments, length: offset }
32
+ }
33
+
34
+ /**
35
+ * Removes complete leading assignments that explicitly clear one environment variable.
36
+ *
37
+ * @param {string} command command text
38
+ * @param {string} name environment variable
39
+ * @param {string} [platform] target platform
40
+ * @returns {string} command without empty assignments
41
+ */
42
+ function removeEmptyLiteralEnvironmentAssignments (command, name, platform = process.platform) {
43
+ const source = String(command || '')
44
+ const prefix = parseLiteralEnvironmentPrefix(source)
45
+ const retained = prefix.assignments.filter(assignment => {
46
+ return !environmentNamesEqual(assignment.name, name, platform) || assignment.value !== ''
47
+ })
48
+ if (retained.length === prefix.assignments.length) return source
49
+ return [...retained.map(assignment => assignment.source), source.slice(prefix.length)]
50
+ .filter(Boolean)
51
+ .join(' ')
52
+ }
53
+
54
+ module.exports = {
55
+ parseLiteralEnvironmentPrefix,
56
+ removeEmptyLiteralEnvironmentAssignments,
57
+ }
@@ -76,18 +76,14 @@ function getManifestPaths (manifest) {
76
76
  paths.push(
77
77
  [`${prefix}.project.root`, framework.project?.root],
78
78
  [`${prefix}.project.packageJson`, framework.project?.packageJson],
79
+ [`${prefix}.validation.runner`, framework.validation?.runner],
80
+ [`${prefix}.validation.testFile`, framework.validation?.testFile],
79
81
  [`${prefix}.ciWiring.configFile`, framework.ciWiring?.configFile],
80
82
  [`${prefix}.ciWiring.workingDirectory`, framework.ciWiring?.workingDirectory]
81
83
  )
82
84
  for (const [index, configFile] of (framework.project?.configFiles || []).entries()) {
83
85
  paths.push([`${prefix}.project.configFiles[${index}]`, configFile])
84
86
  }
85
- for (const [name, command] of getCommands(framework)) {
86
- paths.push([`${prefix}.${name}.cwd`, command.cwd])
87
- for (const [outputIndex, outputPath] of (command.outputPaths || []).entries()) {
88
- paths.push([`${prefix}.${name}.outputPaths[${outputIndex}]`, outputPath])
89
- }
90
- }
91
87
 
92
88
  const strategy = framework.generatedTestStrategy
93
89
  paths.push([`${prefix}.generatedTestStrategy.testDirectory`, strategy?.testDirectory])
@@ -109,22 +105,6 @@ function getManifestPaths (manifest) {
109
105
  return paths
110
106
  }
111
107
 
112
- function getCommands (framework) {
113
- const commands = []
114
- for (const name of ['existingTestCommand', 'ciWiringCommand']) {
115
- if (framework[name]) commands.push([name, framework[name]])
116
- }
117
- for (const [index, command] of (framework.setup?.commands || []).entries()) {
118
- commands.push([`setup.commands[${index}]`, command])
119
- }
120
- for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
121
- if (scenario?.runCommand) {
122
- commands.push([`generatedTestStrategy.scenarios[${index}].runCommand`, scenario.runCommand])
123
- }
124
- }
125
- return commands
126
- }
127
-
128
108
  function isPathInside (root, filename) {
129
109
  const relative = path.relative(root, filename)
130
110
  return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))