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
@@ -1,480 +1,251 @@
1
1
  'use strict'
2
2
 
3
- /* eslint-disable eslint-rules/eslint-process-env */
4
-
5
3
  const crypto = require('node:crypto')
6
4
  const fs = require('node:fs')
7
5
  const path = require('node:path')
8
6
 
9
7
  const { bindApprovedExecutable, getApprovedExecutable } = require('./executable-approval')
10
- const { getCiWiringCommand, getLocalValidationCommand } = require('./local-command')
8
+ const { getManifestCommands } = require('./runner-command')
11
9
 
12
10
  /**
13
- * Returns an executable that is unavailable for a structured command.
11
+ * Returns the unavailable file for a direct command.
14
12
  *
15
- * @param {object} command manifest command
13
+ * @param {object} command validator-owned command
16
14
  * @returns {string|undefined} unavailable executable
17
15
  */
18
16
  function getUnavailableExecutable (command) {
19
- for (const executableCommand of getExecutableCommands(command)) {
20
- const executable = getExecutable(executableCommand)
21
- if (executable && !resolveExecutable(executable, executableCommand)) return executable
17
+ if (!isDirectRunnerCommand(command)) return command?.argv?.[0] || 'invalid direct-runner command'
18
+ for (const filename of [process.execPath, command.argv[1]]) {
19
+ if (!isRegularFile(filename)) return filename
22
20
  }
23
21
  }
24
22
 
25
23
  /**
26
- * Reads the executable used to start a structured command.
27
- *
28
- * @param {object} command manifest command
29
- * @returns {string|undefined} command executable
30
- */
31
- function getExecutable (command) {
32
- if (!command?.usesShell) return command?.argv?.[0]
33
- if (typeof command.shell === 'string' && command.shell.trim()) return command.shell.trim()
34
- return process.platform === 'win32'
35
- ? process.env.ComSpec || process.env.COMSPEC || 'cmd.exe'
36
- : process.env.SHELL || '/bin/sh'
37
- }
38
-
39
- /**
40
- * Resolves an executable from the command working directory or PATH.
24
+ * Returns the trusted Node.js executable used for every validation command.
41
25
  *
42
- * @param {string} executable executable name or path
43
- * @param {object} command manifest command
44
- * @returns {boolean} whether the executable can be resolved
45
- */
46
- function resolveExecutable (executable, command) {
47
- if (isExplicitExecutablePath(executable)) {
48
- return isExecutable(path.resolve(command.cwd, executable))
49
- }
50
-
51
- const environmentPath = getEnvironmentPath(command)
52
- const extensions = getExecutableExtensions()
53
-
54
- for (const directory of environmentPath.split(path.delimiter)) {
55
- if (!directory) continue
56
- const resolvedDirectory = path.resolve(command.cwd, directory)
57
- for (const extension of extensions) {
58
- const filename = path.join(resolvedDirectory, `${executable}${extension}`)
59
- if (isExecutable(filename)) return true
60
- }
61
- }
62
- return false
63
- }
64
-
65
- /**
66
- * Resolves the filesystem path used for a structured command executable.
67
- *
68
- * @param {object} command manifest command
69
- * @returns {string|undefined} resolved executable path
26
+ * @param {object} command validator-owned command
27
+ * @returns {string|undefined} Node.js executable
70
28
  */
71
29
  function getResolvedExecutable (command) {
72
- const executable = getExecutable(command)
73
- if (!executable) return
74
-
75
- if (isExplicitExecutablePath(executable)) {
76
- const filename = path.resolve(command.cwd, executable)
77
- return isExecutable(filename) ? filename : undefined
78
- }
79
-
80
- const environmentPath = getEnvironmentPath(command)
81
- const extensions = getExecutableExtensions()
82
-
83
- for (const directory of environmentPath.split(path.delimiter)) {
84
- if (!directory) continue
85
- const resolvedDirectory = path.resolve(command.cwd, directory)
86
- for (const extension of extensions) {
87
- const filename = path.join(resolvedDirectory, `${executable}${extension}`)
88
- if (isExecutable(filename)) return filename
89
- }
90
- }
30
+ return isDirectRunnerCommand(command) && isRegularFile(process.execPath)
31
+ ? fs.realpathSync(process.execPath)
32
+ : undefined
91
33
  }
92
34
 
93
35
  /**
94
- * Returns the PATH used by a command, preserving an explicitly empty value.
36
+ * Fingerprints and binds every runnable framework's Node.js and runner files.
95
37
  *
96
- * @param {object} command manifest command
97
- * @returns {string} command PATH
98
- */
99
- function getEnvironmentPath (command) {
100
- if (Object.hasOwn(command.env || {}, 'PATH')) return command.env.PATH || ''
101
- return process.env.PATH || ''
102
- }
103
-
104
- /**
105
- * Binds every executable selected by an approvable manifest command to its canonical file identity.
106
- *
107
- * @param {object} manifest loaded manifest
108
- * @returns {object[]} sorted executable identities included in approval material
38
+ * @param {object} manifest normalized manifest
39
+ * @returns {object[]} executable identities
109
40
  */
110
41
  function bindManifestExecutables (manifest) {
111
42
  const identities = []
112
- const identitiesByPath = new Map()
113
- for (const [label, command, sourceCommand] of getManifestCommands(manifest)) {
114
- const identity = getCommandExecutableIdentity(command, identitiesByPath)
115
- if (!identity) continue
116
- bindApprovedExecutable(command, identity)
117
- if (sourceCommand !== command) bindApprovedExecutable(sourceCommand, identity)
118
- identities.push({ label, ...identity })
43
+ for (const framework of manifest.frameworks || []) {
44
+ if (framework.status !== 'runnable') continue
45
+ const command = getManifestCommands({ frameworks: [framework] })[0]?.[1]
46
+ try {
47
+ const identity = getCommandExecutableIdentity(command, manifest.repository.root)
48
+ bindApprovedExecutable(framework.validation, identity)
49
+ identities.push({ id: `framework:${framework.id}`, ...identity })
50
+ } catch (error) {
51
+ identities.push({
52
+ id: `framework:${framework.id}`,
53
+ unavailable: true,
54
+ reason: error?.message || String(error),
55
+ })
56
+ }
119
57
  }
120
- return identities.sort((left, right) => left.label.localeCompare(right.label))
58
+ return identities
121
59
  }
122
60
 
123
61
  /**
124
- * Verifies and returns the canonical executable and approved invocation name used to spawn it.
62
+ * Resolves a checksum-approved executable immediately before spawn.
125
63
  *
126
- * @param {object} command command about to execute
127
- * @param {{requireApproval?: boolean}} [options] verification options
128
- * @returns {{argv0: string, path: string}} approved launch identity
64
+ * @param {object} command validator-owned command
65
+ * @param {object} [options] resolution options
66
+ * @param {boolean} [options.requireApproval] whether approval is mandatory
67
+ * @returns {{argv0: string, path: string}} spawn executable
129
68
  */
130
69
  function getExecutableForSpawn (command, options = {}) {
131
70
  const approved = getApprovedExecutable(command)
132
- const current = getCommandExecutableIdentity(command)
133
- if (!approved) {
134
- if (options.requireApproval) {
135
- throw new Error('The selected command executable was not covered by the approved execution plan.')
136
- }
137
- if (!current) throw new Error(`Command executable is unavailable: ${getExecutable(command)}`)
138
- return getLaunchIdentity(current)
71
+ if (options.requireApproval && !approved) {
72
+ throw new Error('Direct-runner command is not bound to the approved execution plan.')
139
73
  }
140
- if (!areExecutableIdentitiesEqual(current, approved)) {
141
- throw new Error(
142
- 'The selected command executable changed after approval. Render and approve a fresh execution plan.'
143
- )
74
+ const current = getCommandExecutableIdentity(command, approved?.repositoryRoot)
75
+ if (approved && !areExecutableIdentitiesEqual(current, approved)) {
76
+ throw new Error('The approved Node.js or test-runner executable changed after approval.')
144
77
  }
145
- return getLaunchIdentity(approved)
78
+ return { argv0: process.execPath, path: current.path }
146
79
  }
147
80
 
148
81
  /**
149
- * Resolves a command launcher and every executable delegated through env wrappers.
82
+ * Builds the executable identity for one direct-runner command.
150
83
  *
151
- * @param {object} command manifest command
152
- * @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
153
- * @returns {{delegated?: object[], invocationPath: string, path: string, sha256: string}|undefined} identity tree
84
+ * @param {object} command validator-owned command
85
+ * @param {string} repositoryRoot repository root
86
+ * @returns {object} executable identity
154
87
  */
155
- function getCommandExecutableIdentity (command, identitiesByPath) {
156
- const identities = []
157
- for (const executableCommand of getExecutableCommands(command)) {
158
- const identity = getExecutableIdentity(executableCommand, identitiesByPath)
159
- if (!identity) return
160
- identities.push(identity)
88
+ function getCommandExecutableIdentity (command, repositoryRoot) {
89
+ if (!isDirectRunnerCommand(command)) {
90
+ throw new Error('Only validator-owned `node <contained-runner> ...` commands may execute.')
161
91
  }
162
92
 
163
- const [launcher, ...delegated] = identities
164
- return delegated.length === 0 ? launcher : { ...launcher, delegated }
165
- }
166
-
167
- /**
168
- * Checks whether the launcher and delegated executable identities still match approval.
169
- *
170
- * @param {object|undefined} current current identity tree
171
- * @param {object|undefined} approved approved identity tree
172
- * @returns {boolean} whether every executable matches
173
- */
174
- function areExecutableIdentitiesEqual (current, approved) {
175
- if (!current || !approved) return false
176
- const currentIdentities = [current, ...(current.delegated || [])]
177
- const approvedIdentities = [approved, ...(approved.delegated || [])]
178
- if (currentIdentities.length !== approvedIdentities.length) return false
179
-
180
- return currentIdentities.every((identity, index) => {
181
- const expected = approvedIdentities[index]
182
- return identity.invocationPath === expected.invocationPath && identity.path === expected.path &&
183
- identity.sha256 === expected.sha256
184
- })
185
- }
186
-
187
- /**
188
- * Expands nested env wrappers into the commands whose executables they delegate to.
189
- *
190
- * @param {object} command manifest command
191
- * @returns {object[]} launcher followed by delegated commands
192
- */
193
- function getExecutableCommands (command) {
194
- const commands = [command]
195
- let current = command
196
- while (!current.usesShell && isEnvExecutable(current.argv?.[0])) {
197
- current = getEnvDelegatedCommand(current)
198
- commands.push(current)
93
+ repositoryRoot ||= command.cwd
94
+ const root = fs.realpathSync(repositoryRoot)
95
+ const node = getFileIdentity(process.execPath)
96
+ const runner = getFileIdentity(command.argv[1])
97
+ if (!isPathInside(root, runner.path)) {
98
+ throw new Error(`Test-runner executable resolves outside repository.root: ${command.argv[1]}`)
199
99
  }
200
- return commands
201
- }
202
-
203
- /**
204
- * Returns the command executed by an env wrapper with its effective PATH and working directory.
205
- *
206
- * @param {object} command env wrapper command
207
- * @returns {object} delegated command
208
- */
209
- function getEnvDelegatedCommand (command) {
210
- const parsed = parseArgv(command.argv)
211
- if (parsed.unsupportedEnvOption) {
212
- throw new Error(
213
- `Cannot approve env-wrapped command because option "${parsed.unsupportedEnvOption}" prevents reliable ` +
214
- 'executable fingerprinting.'
215
- )
216
- }
217
- if (parsed.commandIndex >= command.argv.length) {
218
- throw new Error('Cannot approve env-wrapped command because it does not identify a delegated executable.')
219
- }
220
- const delegatedExecutable = command.argv[parsed.commandIndex]
221
- const requiresPathLookup = !isExplicitExecutablePath(delegatedExecutable)
222
- if (requiresPathLookup && parsed.unsetEnvNames.some(name => name.toUpperCase() === 'PATH')) {
223
- throw new Error('Cannot approve env-wrapped command because it removes PATH before selecting its executable.')
224
- }
225
-
226
- const env = parsed.ignoreEnvironment ? { ...parsed.prefixEnv } : { ...command.env, ...parsed.prefixEnv }
227
- if (requiresPathLookup && parsed.ignoreEnvironment && !Object.hasOwn(env, 'PATH')) {
228
- throw new Error(
229
- 'Cannot approve env-wrapped command because it clears the environment without declaring an explicit PATH.'
230
- )
231
- }
232
-
233
100
  return {
234
- ...command,
235
- argv: command.argv.slice(parsed.commandIndex),
236
- cwd: parsed.workingDirectory ? path.resolve(command.cwd, parsed.workingDirectory) : command.cwd,
237
- env,
101
+ argv0: process.execPath,
102
+ path: node.path,
103
+ sha256: node.sha256,
104
+ repositoryRoot,
105
+ entrypoints: [runner],
238
106
  }
239
107
  }
240
108
 
241
109
  /**
242
- * Selects the verified path and invocation name used to launch an executable.
110
+ * Compares current and approved executable identities.
243
111
  *
244
- * @param {{invocationPath: string, path: string}} identity verified executable identity
245
- * @returns {{argv0: string, path: string}} executable launch identity
112
+ * @param {object} current current identity
113
+ * @param {object} approved approved identity
114
+ * @returns {boolean} exact identity match
246
115
  */
247
- function getLaunchIdentity (identity) {
248
- return {
249
- argv0: identity.invocationPath,
250
- // Windows package-manager shims rely on their invoked path. The canonical target is still verified above.
251
- path: process.platform === 'win32' ? identity.invocationPath : identity.path,
116
+ function areExecutableIdentitiesEqual (current, approved) {
117
+ return current.argv0 === approved.argv0 &&
118
+ current.path === approved.path &&
119
+ current.sha256 === approved.sha256 &&
120
+ current.entrypoints?.length === approved.entrypoints?.length &&
121
+ current.entrypoints.every((entrypoint, index) => {
122
+ const expected = approved.entrypoints[index]
123
+ return entrypoint.path === expected.path && entrypoint.sha256 === expected.sha256
124
+ })
125
+ }
126
+
127
+ /**
128
+ * Splits NODE_OPTIONS without invoking a shell or expanding variables.
129
+ *
130
+ * @param {string} source NODE_OPTIONS source
131
+ * @returns {string[]} Node.js arguments
132
+ */
133
+ function splitNodeOptions (source) {
134
+ const args = []
135
+ let value = ''
136
+ let started = false
137
+ let quote
138
+
139
+ for (let index = 0; index < String(source).length; index++) {
140
+ const character = source[index]
141
+ if (character === '\0') throw new Error('NODE_OPTIONS contains a null byte.')
142
+ if (!quote && /\s/.test(character)) {
143
+ if (started) args.push(value)
144
+ value = ''
145
+ started = false
146
+ continue
147
+ }
148
+ if (character === '"' || character === "'") {
149
+ if (!quote) {
150
+ quote = character
151
+ started = true
152
+ continue
153
+ }
154
+ if (quote === character) {
155
+ quote = undefined
156
+ continue
157
+ }
158
+ }
159
+ const next = source[index + 1]
160
+ if (character === '\\' && next && (next === quote || /[\s'"\\]/.test(next))) {
161
+ value += source[++index]
162
+ } else {
163
+ value += character
164
+ }
165
+ started = true
252
166
  }
167
+ if (quote) throw new Error('NODE_OPTIONS contains an unterminated quoted value.')
168
+ if (started) args.push(value)
169
+ return args
253
170
  }
254
171
 
255
172
  /**
256
- * Resolves one command executable to a stable canonical path and content digest.
173
+ * Returns whether a value is an explicit filesystem executable path.
257
174
  *
258
- * @param {object} command manifest command
259
- * @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
260
- * @returns {{invocationPath: string, path: string, sha256: string}|undefined} executable identity
175
+ * @param {string} value candidate value
176
+ * @param {string} [platform] target platform
177
+ * @returns {boolean} whether the value includes explicit path syntax
261
178
  */
262
- function getExecutableIdentity (command, identitiesByPath) {
263
- const resolved = getResolvedExecutable(command)
264
- if (!resolved) return
265
- const canonicalPath = fs.realpathSync(resolved)
266
- const cached = identitiesByPath?.get(canonicalPath)
267
- if (cached) return { invocationPath: resolved, ...cached }
268
- const stat = fs.statSync(canonicalPath)
269
- if (!stat.isFile()) return
270
- const canonicalIdentity = {
271
- path: canonicalPath,
272
- sha256: crypto.createHash('sha256').update(fs.readFileSync(canonicalPath)).digest('hex'),
273
- }
274
- identitiesByPath?.set(canonicalPath, canonicalIdentity)
275
- return { invocationPath: resolved, ...canonicalIdentity }
179
+ function isExplicitExecutablePath (value, platform = process.platform) {
180
+ if (typeof value !== 'string') return false
181
+ if (path.isAbsolute(value) || value.startsWith('.')) return true
182
+ return platform === 'win32' ? /[\\/]/.test(value) : value.includes('/')
276
183
  }
277
184
 
278
185
  /**
279
- * Enumerates every executable-bearing manifest command with a stable approval label.
186
+ * Returns whether a command has the only executable shape the validator supports.
280
187
  *
281
- * @param {object} manifest loaded manifest
282
- * @returns {Array<[string, object]>} labeled commands
188
+ * @param {object} command command candidate
189
+ * @returns {boolean} whether it is a direct runner
283
190
  */
284
- function getManifestCommands (manifest) {
285
- const commands = []
286
- for (const framework of manifest.frameworks || []) {
287
- const prefix = `framework:${framework.id}`
288
- const basicSource = framework.existingTestCommand
289
- if (basicSource) {
290
- commands.push([`${prefix}:basic-reporting`, getLocalValidationCommand(framework, basicSource), basicSource])
291
- }
292
- if (framework.ciWiringCommand) {
293
- commands.push([`${prefix}:ci-wiring`, getCiWiringCommand(framework), framework.ciWiringCommand])
294
- }
295
- for (const [index, command] of (framework.setup?.commands || []).entries()) {
296
- commands.push([`${prefix}:setup:${index}`, command, command])
297
- }
298
- for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
299
- if (scenario.runCommand) {
300
- commands.push([
301
- `${prefix}:generated:${index}`,
302
- getLocalValidationCommand(framework, scenario.runCommand),
303
- scenario.runCommand,
304
- ])
305
- }
306
- }
307
- }
308
- return commands
191
+ function isDirectRunnerCommand (command) {
192
+ return command?.usesShell === false &&
193
+ Array.isArray(command.argv) &&
194
+ command.argv.length >= 3 &&
195
+ command.argv[0] === process.execPath &&
196
+ path.isAbsolute(command.argv[1])
309
197
  }
310
198
 
311
199
  /**
312
- * Detects explicit executable paths using platform path syntax.
200
+ * Fingerprints a regular file.
313
201
  *
314
- * @param {string} executable executable name or path
315
- * @param {string} [platform] target platform
316
- * @returns {boolean} whether the value is a path rather than a PATH name
202
+ * @param {string} filename file path
203
+ * @returns {{path: string, sha256: string}} file identity
317
204
  */
318
- function isExplicitExecutablePath (executable, platform = process.platform) {
319
- return path.isAbsolute(executable) || executable.includes('/') || (platform === 'win32' && executable.includes('\\'))
320
- }
321
-
322
- function getExecutableExtensions () {
323
- if (process.platform !== 'win32') return ['']
324
- return ['', ...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')]
205
+ function getFileIdentity (filename) {
206
+ const physical = fs.realpathSync(filename)
207
+ const stat = fs.lstatSync(physical)
208
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Executable must be a regular file: ${filename}`)
209
+ return {
210
+ path: physical,
211
+ sha256: crypto.createHash('sha256').update(fs.readFileSync(physical)).digest('hex'),
212
+ }
325
213
  }
326
214
 
327
215
  /**
328
- * Checks whether a filesystem entry can be executed.
216
+ * Returns whether a path resolves to a regular file.
329
217
  *
330
- * @param {string} filename executable candidate
331
- * @returns {boolean} whether the candidate is executable
218
+ * @param {string} filename candidate path
219
+ * @returns {boolean} whether the file is available
332
220
  */
333
- function isExecutable (filename) {
221
+ function isRegularFile (filename) {
334
222
  try {
335
- fs.accessSync(filename, fs.constants.X_OK)
336
- return true
223
+ return fs.statSync(filename).isFile()
337
224
  } catch {
338
225
  return false
339
226
  }
340
227
  }
341
228
 
342
229
  /**
343
- * Parses structured env wrappers and runtime plumbing without executing them.
230
+ * Checks path containment.
344
231
  *
345
- * @param {string[]} argv command arguments
346
- * @returns {object} parsed wrapper details
232
+ * @param {string} root root path
233
+ * @param {string} filename candidate path
234
+ * @returns {boolean} whether the path is contained
347
235
  */
348
- function parseArgv (argv) {
349
- const result = {
350
- ignoreEnvironment: false,
351
- prefixAssignments: [],
352
- prefixEnv: {},
353
- unsetEnvNames: [],
354
- commandIndex: 0,
355
- corepackIndex: -1,
356
- pathAdjusted: false,
357
- unsupportedEnvOption: undefined,
358
- workingDirectory: undefined,
359
- }
360
-
361
- if (!Array.isArray(argv) || argv.length === 0) return result
362
-
363
- let index = 0
364
- if (isEnvExecutable(argv[index])) {
365
- index++
366
- while (index < argv.length) {
367
- const option = argv[index]
368
- if (option === '--') {
369
- index++
370
- break
371
- }
372
- if (option === '-' || option === '-i' || option === '--ignore-environment') {
373
- result.ignoreEnvironment = true
374
- index++
375
- continue
376
- }
377
- if (option === '-u' || option === '--unset') {
378
- if (typeof argv[index + 1] === 'string') result.unsetEnvNames.push(argv[index + 1])
379
- index += 2
380
- continue
381
- }
382
- const unsetMatch = /^(?:-u|--unset=)(.+)$/.exec(option)
383
- if (unsetMatch) {
384
- result.unsetEnvNames.push(unsetMatch[1])
385
- index++
386
- continue
387
- }
388
- if (option === '-C' || option === '--chdir') {
389
- if (typeof argv[index + 1] !== 'string') {
390
- result.unsupportedEnvOption = option
391
- break
392
- }
393
- result.workingDirectory = argv[index + 1]
394
- index += 2
395
- continue
396
- }
397
- const chdirMatch = /^(?:-C(.+)|--chdir=(.+))$/.exec(option)
398
- if (chdirMatch) {
399
- result.workingDirectory = chdirMatch[1] || chdirMatch[2]
400
- index++
401
- continue
402
- }
403
- if (option === '-S' || option === '--split-string' || /^(?:-S.+|--split-string=.+)$/.test(option)) {
404
- result.unsupportedEnvOption = option
405
- break
406
- }
407
- if (isSupportedEnvFlag(option)) {
408
- index++
409
- continue
410
- }
411
- if (option.startsWith('-')) {
412
- result.unsupportedEnvOption = option
413
- break
414
- }
415
- if (!isEnvAssignment(option)) break
416
-
417
- const assignment = argv[index]
418
- const equalsIndex = assignment.indexOf('=')
419
- const name = assignment.slice(0, equalsIndex)
420
- const value = assignment.slice(equalsIndex + 1)
421
- result.prefixEnv[name] = value
422
-
423
- if (name === 'PATH') {
424
- result.pathAdjusted = true
425
- } else {
426
- result.prefixAssignments.push(assignment)
427
- }
428
- index++
429
- }
430
- }
431
-
432
- result.commandIndex = index
433
-
434
- if (isNodeExecutable(argv[index]) && isCorepackScript(argv[index + 1]) && argv[index + 2]) {
435
- result.corepackIndex = index + 1
436
- }
437
-
438
- return result
439
- }
440
-
441
- function isSupportedEnvFlag (option) {
442
- return /^(?:-0|-v|--null|--debug|--help|--version|--list-signal-handling)$/.test(option) ||
443
- /^--(?:block|default|ignore)-signal(?:=.*)?$/.test(option)
444
- }
445
-
446
- function isEnvExecutable (value) {
447
- const name = getExecutableName(value)
448
- return name === 'env' || name === 'env.exe'
449
- }
450
-
451
- function isEnvAssignment (value) {
452
- return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value)
453
- }
454
-
455
- function isNodeExecutable (value = '') {
456
- const name = getExecutableName(value)
457
- return name === 'node' || name === 'node.exe'
458
- }
459
-
460
- function isCorepackScript (value = '') {
461
- const name = getExecutableName(value)
462
- return name === 'corepack' || name === 'corepack.exe' || name === 'corepack.js'
463
- }
464
-
465
- function getExecutableName (value = '') {
466
- return String(value).split(/[\\/]/).pop().toLowerCase()
236
+ function isPathInside (root, filename) {
237
+ const relative = path.relative(path.resolve(root), path.resolve(filename))
238
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
467
239
  }
468
240
 
469
241
  module.exports = {
242
+ areExecutableIdentitiesEqual,
470
243
  bindManifestExecutables,
471
- getApprovedExecutable,
244
+ getCommandExecutableIdentity,
472
245
  getExecutableForSpawn,
473
246
  getManifestCommands,
474
247
  getResolvedExecutable,
475
248
  getUnavailableExecutable,
476
- isEnvExecutable,
477
249
  isExplicitExecutablePath,
478
- isNodeExecutable,
479
- parseArgv,
250
+ splitNodeOptions,
480
251
  }