dd-trace 5.117.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 (83) 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 +2 -2
  45. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  48. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  49. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  50. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  51. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  52. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  53. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  54. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  55. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  57. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  58. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  59. package/packages/dd-trace/src/exporter.js +16 -9
  60. package/packages/dd-trace/src/llmobs/index.js +9 -1
  61. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  62. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  63. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  64. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  65. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  66. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  67. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  68. package/packages/dd-trace/src/llmobs/util.js +56 -3
  69. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  70. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  72. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  73. package/packages/dd-trace/src/plugins/index.js +1 -0
  74. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  75. package/packages/dd-trace/src/span_processor.js +1 -1
  76. package/packages/dd-trace/src/span_stats.js +22 -4
  77. package/ci/test-optimization-validation/command-suitability.js +0 -471
  78. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  79. package/ci/test-optimization-validation/init-probe.js +0 -246
  80. package/ci/test-optimization-validation/late-initialization.js +0 -63
  81. package/ci/test-optimization-validation/local-command.js +0 -163
  82. package/ci/test-optimization-validation/setup-runner.js +0 -97
  83. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -0,0 +1,119 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+
5
+ const CUCUMBER_PACKAGE = '@cucumber/cucumber'
6
+ const CONFIG_PATTERN = /^cucumber\.(?:[cm]?js|json|ya?ml)$/
7
+ const GENERATED_STEPS_FILENAME = 'dd-test-optimization-validation.steps.cjs'
8
+
9
+ /**
10
+ * Reports whether a file follows the Cucumber feature convention.
11
+ *
12
+ * @param {string} filename candidate filename
13
+ * @returns {boolean} whether the candidate can be selected by Cucumber
14
+ */
15
+ function isTestFile (filename) {
16
+ return filename.endsWith('.feature')
17
+ }
18
+
19
+ /**
20
+ * Counts statically declared Cucumber scenarios in a feature.
21
+ *
22
+ * @param {string} source feature source
23
+ * @returns {number} declared scenario count
24
+ */
25
+ function getScenarioCount (source) {
26
+ return [...source.matchAll(/^\s*Scenario(?: Outline)?:\s*\S/gm)].length
27
+ }
28
+
29
+ /**
30
+ * Returns validator-owned Cucumber feature source for one generated scenario.
31
+ *
32
+ * @param {object} input generated source input
33
+ * @param {string} input.testName generated scenario name
34
+ * @returns {string} canonical generated feature source
35
+ */
36
+ function getGeneratedTestContent ({ testName }) {
37
+ return [
38
+ 'Feature: Datadog Test Optimization validation',
39
+ '',
40
+ ` Scenario: ${testName}`,
41
+ ` Given the Datadog validation scenario ${JSON.stringify(testName)}`,
42
+ ].join('\n')
43
+ }
44
+
45
+ /**
46
+ * Returns the validator-owned Cucumber step definitions shared by generated scenarios.
47
+ *
48
+ * @returns {string} canonical generated step-definition source
49
+ */
50
+ function getGeneratedStepsContent () {
51
+ return [
52
+ "'use strict'",
53
+ '',
54
+ `const { Given } = require(${JSON.stringify(CUCUMBER_PACKAGE)})`,
55
+ '',
56
+ 'let atrAttempt = 0',
57
+ '',
58
+ "Given('the Datadog validation scenario {string}', function (scenario) {",
59
+ " if (scenario === 'atr-fail-once' && atrAttempt++ === 0) {",
60
+ " throw new Error('dd-test-optimization-validation atr first failure')",
61
+ ' }',
62
+ '})',
63
+ ].join('\n')
64
+ }
65
+
66
+ /**
67
+ * Returns the generated Cucumber step-definition path for a feature directory.
68
+ *
69
+ * @param {string} testDirectory generated feature directory
70
+ * @returns {string} generated step-definition path
71
+ */
72
+ function getGeneratedStepsPath (testDirectory) {
73
+ return path.join(testDirectory, GENERATED_STEPS_FILENAME)
74
+ }
75
+
76
+ /**
77
+ * Returns Cucumber arguments that select one existing feature.
78
+ *
79
+ * @param {string} filename selected Cucumber feature
80
+ * @returns {string[]} focused Cucumber arguments
81
+ */
82
+ function getFocusedTestArgs (filename) {
83
+ return [filename, '--format', 'progress']
84
+ }
85
+
86
+ /**
87
+ * Returns Cucumber arguments for one isolated generated scenario.
88
+ *
89
+ * @param {string} filename generated Cucumber feature
90
+ * @param {string} stepsFile generated Cucumber step definitions
91
+ * @returns {string[]} generated scenario arguments
92
+ */
93
+ function getGeneratedTestArgs (filename, stepsFile) {
94
+ return ['--require', stepsFile, '--format', 'progress', filename]
95
+ }
96
+
97
+ /**
98
+ * Extracts the executed-scenario count from the final Cucumber summary.
99
+ *
100
+ * @param {string} output Cucumber output without ANSI escapes
101
+ * @returns {number|null} final scenario count when present
102
+ */
103
+ function getObservedTestCount (output) {
104
+ let count = null
105
+ for (const match of output.matchAll(/\b(\d+)\s+scenarios?\b/gi)) count = Number(match[1])
106
+ return count
107
+ }
108
+
109
+ module.exports = {
110
+ CONFIG_PATTERN,
111
+ getFocusedTestArgs,
112
+ getGeneratedStepsContent,
113
+ getGeneratedStepsPath,
114
+ getGeneratedTestArgs,
115
+ getGeneratedTestContent,
116
+ getObservedTestCount,
117
+ getScenarioCount,
118
+ isTestFile,
119
+ }
@@ -0,0 +1,107 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+
5
+ const CONFIG_PATTERN = /^cypress(?:\.config)?\.(?:[cm]?[jt]s|json)$/
6
+ const TEST_FILE_PATTERN = /(?:\.cy\.[cm]?[jt]sx?|\.(?:spec|test)\.[cm]?[jt]sx?)$/
7
+
8
+ /**
9
+ * Reports whether a file follows a Cypress spec convention.
10
+ *
11
+ * @param {string} filename candidate filename
12
+ * @param {string} directory candidate parent directory
13
+ * @param {string} projectRoot detected project root
14
+ * @returns {boolean} whether the candidate can be selected by Cypress
15
+ */
16
+ function isTestFile (filename, directory, projectRoot) {
17
+ if (/\.cy\.[cm]?[jt]sx?$/.test(filename)) return true
18
+ const relativeDirectory = new Set(path.relative(projectRoot, directory).split(path.sep))
19
+ return relativeDirectory.has('cypress') &&
20
+ (relativeDirectory.has('e2e') || relativeDirectory.has('integration')) &&
21
+ TEST_FILE_PATTERN.test(filename)
22
+ }
23
+
24
+ /**
25
+ * Returns the complete suffix that a generated Cypress spec must preserve.
26
+ *
27
+ * @param {string} filename representative Cypress spec
28
+ * @returns {string} generated spec suffix
29
+ */
30
+ function getTestExtension (filename) {
31
+ return TEST_FILE_PATTERN.exec(path.basename(filename))?.[0] || '.cy.js'
32
+ }
33
+
34
+ /**
35
+ * Returns validator-owned Cypress source for one generated scenario.
36
+ *
37
+ * @param {object} input generated source input
38
+ * @param {string} input.scenarioId generated scenario id
39
+ * @param {string} input.testName generated test name
40
+ * @returns {string} canonical generated Cypress source
41
+ */
42
+ function getGeneratedTestContent ({ scenarioId, testName }) {
43
+ const lines = []
44
+ if (scenarioId === 'atr-fail-once') {
45
+ lines.push('let attempt = 0', '')
46
+ }
47
+ lines.push(
48
+ "describe('dd-test-optimization-validation', () => {",
49
+ ` it(${JSON.stringify(testName)}, () => {`,
50
+ scenarioId === 'atr-fail-once'
51
+ ? ' expect(attempt++).to.equal(1)'
52
+ : ' expect(true).to.equal(true)',
53
+ ' })',
54
+ '})'
55
+ )
56
+ return lines.join('\n')
57
+ }
58
+
59
+ /**
60
+ * Returns Cypress arguments that select one existing project spec.
61
+ *
62
+ * @param {string} filename selected Cypress spec
63
+ * @returns {string[]} focused Cypress arguments
64
+ */
65
+ function getFocusedTestArgs (filename) {
66
+ return ['--spec', filename]
67
+ }
68
+
69
+ /**
70
+ * Returns Cypress arguments for one isolated generated scenario.
71
+ *
72
+ * @param {string} filename generated Cypress spec
73
+ * @param {string[]} configurationArgs approved Cypress configuration arguments
74
+ * @returns {string[]} generated scenario arguments
75
+ */
76
+ function getGeneratedTestArgs (filename, configurationArgs) {
77
+ return [
78
+ 'run',
79
+ ...configurationArgs,
80
+ '--spec',
81
+ filename,
82
+ '--config',
83
+ 'video=false,screenshotOnRunFailure=false,retries=0',
84
+ ]
85
+ }
86
+
87
+ /**
88
+ * Extracts the executed-test count from the final Cypress run summary.
89
+ *
90
+ * @param {string} output Cypress output without ANSI escapes
91
+ * @returns {number|null} final test count when present
92
+ */
93
+ function getObservedTestCount (output) {
94
+ let count = null
95
+ for (const match of output.matchAll(/\bTests\s*:\s*(\d+)\b/gi)) count = Number(match[1])
96
+ return count
97
+ }
98
+
99
+ module.exports = {
100
+ CONFIG_PATTERN,
101
+ getFocusedTestArgs,
102
+ getGeneratedTestArgs,
103
+ getGeneratedTestContent,
104
+ getObservedTestCount,
105
+ getTestExtension,
106
+ isTestFile,
107
+ }
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+
5
+ const CONFIG_PATTERN = /^playwright\.config\.[cm]?[jt]s$/
6
+ const TEST_FILE_PATTERN = /(?:\.(?:spec|test)\.[cm]?[jt]sx?)$/
7
+ const GENERATED_CONFIG_FILENAME = 'dd-test-optimization-validation.playwright.config.cjs'
8
+ const VALIDATION_OUTPUT_DIRECTORY = '.dd-test-optimization-validation-playwright-output'
9
+ const PLAYWRIGHT_PACKAGE = '@playwright/test'
10
+
11
+ /**
12
+ * Reports whether a file follows a Playwright Test convention.
13
+ *
14
+ * @param {string} filename candidate filename
15
+ * @param {string} directory candidate parent directory
16
+ * @param {string} projectRoot detected project root
17
+ * @returns {boolean} whether the candidate can be selected by Playwright Test
18
+ */
19
+ function isTestFile (filename, directory, projectRoot) {
20
+ if (!TEST_FILE_PATTERN.test(filename)) return false
21
+ const relative = path.relative(projectRoot, path.join(directory, filename))
22
+ return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
23
+ }
24
+
25
+ /**
26
+ * Returns the complete suffix that a generated Playwright spec must preserve.
27
+ *
28
+ * @param {string} filename representative Playwright spec
29
+ * @returns {string} generated spec suffix
30
+ */
31
+ function getTestExtension (filename) {
32
+ return TEST_FILE_PATTERN.exec(path.basename(filename))?.[0] || '.spec.js'
33
+ }
34
+
35
+ /**
36
+ * Returns validator-owned Playwright source for one generated scenario.
37
+ *
38
+ * @param {object} input generated source input
39
+ * @param {string} input.moduleSystem generated module system
40
+ * @param {string} input.scenarioId generated scenario id
41
+ * @param {string} input.testName generated test name
42
+ * @returns {string} canonical generated Playwright source
43
+ */
44
+ function getGeneratedTestContent ({ moduleSystem, scenarioId, testName }) {
45
+ const assertion = scenarioId === 'atr-fail-once'
46
+ ? ' expect(test.info().retry).toBe(1)'
47
+ : ' expect(true).toBe(true)'
48
+ const imports = moduleSystem === 'commonjs'
49
+ ? `const { expect, test } = require(${JSON.stringify(PLAYWRIGHT_PACKAGE)})`
50
+ : `import { expect, test } from ${JSON.stringify(PLAYWRIGHT_PACKAGE)}`
51
+ return [
52
+ imports,
53
+ '',
54
+ `test(${JSON.stringify(testName)}, async () => {`,
55
+ assertion,
56
+ '})',
57
+ ].join('\n')
58
+ }
59
+
60
+ /**
61
+ * Returns the isolated Playwright config shared by generated validation tests.
62
+ *
63
+ * @returns {string} canonical generated Playwright config
64
+ */
65
+ function getGeneratedConfigContent () {
66
+ return [
67
+ `const { defineConfig } = require(${JSON.stringify(PLAYWRIGHT_PACKAGE)})`,
68
+ '',
69
+ 'module.exports = defineConfig({',
70
+ ' fullyParallel: false,',
71
+ ' forbidOnly: true,',
72
+ " reporter: 'line',",
73
+ ' retries: 0,',
74
+ ' testDir: __dirname,',
75
+ ' workers: 1,',
76
+ '})',
77
+ ].join('\n')
78
+ }
79
+
80
+ /**
81
+ * Returns the generated Playwright config path for a test directory.
82
+ *
83
+ * @param {string} testDirectory generated test directory
84
+ * @returns {string} generated config path
85
+ */
86
+ function getGeneratedConfigPath (testDirectory) {
87
+ return path.join(testDirectory, GENERATED_CONFIG_FILENAME)
88
+ }
89
+
90
+ /**
91
+ * Returns Playwright arguments that select one existing project spec.
92
+ *
93
+ * @param {string} filename selected Playwright spec
94
+ * @returns {string[]} focused Playwright arguments
95
+ */
96
+ function getFocusedTestArgs (filename) {
97
+ return [
98
+ filename,
99
+ '--reporter=line',
100
+ '--workers=1',
101
+ `--output=${getOutputPath(filename)}`,
102
+ ]
103
+ }
104
+
105
+ /**
106
+ * Returns Playwright arguments for one isolated generated scenario.
107
+ *
108
+ * @param {string} filename generated Playwright spec
109
+ * @param {string} configFile generated Playwright config
110
+ * @returns {string[]} generated scenario arguments
111
+ */
112
+ function getGeneratedTestArgs (filename, configFile) {
113
+ return [
114
+ 'test',
115
+ '--config',
116
+ configFile,
117
+ filename,
118
+ '--reporter=line',
119
+ '--workers=1',
120
+ `--output=${getOutputPath(filename)}`,
121
+ ]
122
+ }
123
+
124
+ /**
125
+ * Returns the validator-owned Playwright output directory for a selected spec.
126
+ *
127
+ * @param {string} filename selected or generated Playwright spec
128
+ * @returns {string} absolute output directory
129
+ */
130
+ function getOutputPath (filename) {
131
+ return path.join(path.dirname(filename), VALIDATION_OUTPUT_DIRECTORY)
132
+ }
133
+
134
+ /**
135
+ * Extracts the executed-test count from the final Playwright summary.
136
+ *
137
+ * @param {string} output Playwright output without ANSI escapes
138
+ * @returns {number|null} executed test count when present
139
+ */
140
+ function getObservedTestCount (output) {
141
+ const observed = sumLastMatchCounts(output, [
142
+ /^\s*(\d+)\s+passed\b/gim,
143
+ /^\s*(\d+)\s+failed\b/gim,
144
+ /^\s*(\d+)\s+flaky\b/gim,
145
+ ])
146
+ if (observed !== null) return observed
147
+ return /^\s*\d+\s+skipped\b/im.test(output) ? 0 : null
148
+ }
149
+
150
+ /**
151
+ * Sums the final count for each Playwright summary pattern.
152
+ *
153
+ * @param {string} output Playwright output
154
+ * @param {RegExp[]} patterns summary patterns
155
+ * @returns {number|null} summed count when any pattern matched
156
+ */
157
+ function sumLastMatchCounts (output, patterns) {
158
+ let found = false
159
+ let total = 0
160
+ for (const pattern of patterns) {
161
+ let count
162
+ for (const match of output.matchAll(pattern)) count = Number(match[1])
163
+ if (count === undefined) continue
164
+ found = true
165
+ total += count
166
+ }
167
+ return found ? total : null
168
+ }
169
+
170
+ module.exports = {
171
+ CONFIG_PATTERN,
172
+ getFocusedTestArgs,
173
+ getGeneratedConfigContent,
174
+ getGeneratedConfigPath,
175
+ getGeneratedTestArgs,
176
+ getGeneratedTestContent,
177
+ getObservedTestCount,
178
+ getOutputPath,
179
+ getTestExtension,
180
+ isTestFile,
181
+ }
@@ -15,7 +15,7 @@ const initializedCleanupStrategies = new WeakSet()
15
15
  const authorizedRuntimeCleanupFiles = new Map()
16
16
  const writtenGeneratedFiles = new Map()
17
17
 
18
- function writeGeneratedFiles (framework) {
18
+ function writeGeneratedFiles (framework, scenario) {
19
19
  const strategy = framework.generatedTestStrategy
20
20
  if (!strategy || !['planned', 'verified'].includes(strategy.status)) {
21
21
  return []
@@ -26,8 +26,9 @@ function writeGeneratedFiles (framework) {
26
26
  initializeRuntimeCleanupFiles(framework, strategy)
27
27
 
28
28
  const written = []
29
+ const files = getScenarioFiles(strategy, scenario)
29
30
  try {
30
- for (const file of strategy.files || []) {
31
+ for (const file of files) {
31
32
  const filename = validateGeneratedFilePath(framework, file.path)
32
33
  validateContentLines(file.contentLines, filename)
33
34
  const content = `${file.contentLines.join('\n')}\n`
@@ -62,14 +63,47 @@ function writeGeneratedFiles (framework) {
62
63
  return written
63
64
  }
64
65
 
66
+ /**
67
+ * Returns one scenario file plus adapter support files, or every file when no scenario is selected.
68
+ *
69
+ * @param {object} strategy generated strategy
70
+ * @param {object|undefined} scenario selected generated scenario
71
+ * @returns {object[]} generated files to write
72
+ */
73
+ function getScenarioFiles (strategy, scenario) {
74
+ if (!scenario) return strategy.files || []
75
+ const scenarioPaths = new Set((strategy.scenarios || []).map(candidate => {
76
+ return path.resolve(candidate.testIdentities[0].file)
77
+ }))
78
+ const selectedPath = path.resolve(scenario.testIdentities[0].file)
79
+ return (strategy.files || []).filter(file => {
80
+ const filename = path.resolve(file.path)
81
+ return filename === selectedPath || !scenarioPaths.has(filename)
82
+ })
83
+ }
84
+
65
85
  function cleanupGeneratedFiles (manifest, { keep = false } = {}) {
66
- if (keep) return
86
+ if (keep) return { status: 'retained_by_request' }
67
87
 
88
+ const outcome = {
89
+ directoriesRemoved: 0,
90
+ directoriesRetained: 0,
91
+ filesRemoved: 0,
92
+ filesRetained: 0,
93
+ }
68
94
  for (const framework of manifest.frameworks || []) {
69
95
  const strategy = framework.generatedTestStrategy
70
- cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true }))
71
- cleanupCreatedDirectories(framework.project.root)
96
+ addCleanupOutcome(
97
+ outcome,
98
+ cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true })),
99
+ 'files'
100
+ )
101
+ addCleanupOutcome(outcome, cleanupCreatedDirectories(framework.project.root), 'directories')
72
102
  }
103
+ outcome.status = outcome.filesRetained > 0 || outcome.directoriesRetained > 0
104
+ ? 'incomplete'
105
+ : 'completed'
106
+ return outcome
73
107
  }
74
108
 
75
109
  /**
@@ -96,21 +130,30 @@ function getMissingDirectories (root, directory) {
96
130
  * @param {string} root project root
97
131
  */
98
132
  function cleanupCreatedDirectories (root) {
133
+ const outcome = { removed: 0, retained: 0 }
99
134
  const resolvedRoot = path.resolve(root)
100
135
  const directories = [...createdGeneratedDirectories.entries()]
101
- .filter(([directory, authorization]) => {
102
- return isPathInside(resolvedRoot, directory) && isCleanupAuthorizationValid(directory, authorization)
103
- })
136
+ .filter(([directory]) => isPathInside(resolvedRoot, directory))
104
137
  .sort(([left], [right]) => right.length - left.length)
105
138
 
106
- for (const [directory] of directories) {
139
+ for (const [directory, authorization] of directories) {
140
+ if (!isCleanupAuthorizationValid(directory, authorization)) {
141
+ if (pathExists(directory)) outcome.retained++
142
+ continue
143
+ }
107
144
  try {
108
145
  fs.rmdirSync(directory)
109
146
  createdGeneratedDirectories.delete(directory)
147
+ outcome.removed++
110
148
  } catch (error) {
111
- if (error.code === 'ENOENT') createdGeneratedDirectories.delete(directory)
149
+ if (error.code === 'ENOENT') {
150
+ createdGeneratedDirectories.delete(directory)
151
+ } else {
152
+ outcome.retained++
153
+ }
112
154
  }
113
155
  }
156
+ return outcome
114
157
  }
115
158
 
116
159
  function cleanupGeneratedRuntimeFiles (framework) {
@@ -166,18 +209,37 @@ function getSafeCleanupPaths (framework, strategy, { includeGeneratedFiles }) {
166
209
  }
167
210
 
168
211
  function cleanupPaths (cleanupPaths) {
212
+ const outcome = { removed: 0, retained: 0 }
169
213
  for (const cleanupPath of cleanupPaths) {
170
214
  try {
171
215
  const authorization = writtenGeneratedFiles.get(cleanupPath) ||
172
216
  authorizedRuntimeCleanupFiles.get(cleanupPath)
173
- if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization)) continue
174
- if (isDirectory(cleanupPath)) continue
217
+ if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization) || isDirectory(cleanupPath)) {
218
+ if (pathExists(cleanupPath)) outcome.retained++
219
+ continue
220
+ }
175
221
  fs.rmSync(cleanupPath, { force: true })
176
222
  writtenGeneratedFiles.delete(cleanupPath)
223
+ outcome.removed++
177
224
  } catch {
178
- // Cleanup should be best-effort. The report will contain the command artifacts.
225
+ if (pathExists(cleanupPath)) outcome.retained++
179
226
  }
180
227
  }
228
+ return outcome
229
+ }
230
+
231
+ function addCleanupOutcome (target, outcome, kind) {
232
+ target[`${kind}Removed`] += outcome.removed
233
+ target[`${kind}Retained`] += outcome.retained
234
+ }
235
+
236
+ function pathExists (filename) {
237
+ try {
238
+ fs.lstatSync(filename)
239
+ return true
240
+ } catch {
241
+ return false
242
+ }
181
243
  }
182
244
 
183
245
  function forgetWrittenGeneratedFiles (filenames) {