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,738 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const { runDiagnosis } = require('../diagnose')
|
|
9
|
+
const { validateManifest } = require('./manifest-schema')
|
|
10
|
+
|
|
11
|
+
const SUPPORTED_SCAFFOLD_FRAMEWORKS = new Set(['jest', 'mocha', 'vitest'])
|
|
12
|
+
const CI_PATHS = [
|
|
13
|
+
'.github/workflows',
|
|
14
|
+
'.gitlab-ci.yml',
|
|
15
|
+
'.circleci/config.yml',
|
|
16
|
+
'.buildkite/pipeline.yml',
|
|
17
|
+
'bitbucket-pipelines.yml',
|
|
18
|
+
'azure-pipelines.yml',
|
|
19
|
+
'Jenkinsfile',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a schema-valid starting manifest without executing project code.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} input scaffold inputs
|
|
26
|
+
* @param {string} input.root repository root
|
|
27
|
+
* @param {Set<string>} [input.frameworks] selected framework ids or kinds
|
|
28
|
+
* @returns {object} validation manifest scaffold
|
|
29
|
+
*/
|
|
30
|
+
function createManifestScaffold ({ root, frameworks = new Set() }) {
|
|
31
|
+
const repositoryRoot = path.resolve(root)
|
|
32
|
+
const diagnosis = runDiagnosis({ root: repositoryRoot, env: {} })
|
|
33
|
+
const selected = diagnosis.eligibleFrameworks.filter(framework => {
|
|
34
|
+
return frameworks.size === 0 || frameworks.has(framework.id) || frameworks.has(framework.id.split(':')[0])
|
|
35
|
+
})
|
|
36
|
+
const unsupported = diagnosis.unsupportedFrameworks.filter(framework => {
|
|
37
|
+
return frameworks.size === 0 || frameworks.has(framework.id) || frameworks.has(framework.id.split(':')[0])
|
|
38
|
+
})
|
|
39
|
+
if (selected.length === 0 && unsupported.length === 0) {
|
|
40
|
+
throw new Error('No test framework was detected for manifest scaffolding.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const manifest = {
|
|
44
|
+
schemaVersion: '1.0',
|
|
45
|
+
generatedAt: new Date().toISOString(),
|
|
46
|
+
repository: {
|
|
47
|
+
root: repositoryRoot,
|
|
48
|
+
gitRemote: null,
|
|
49
|
+
gitSha: null,
|
|
50
|
+
packageManager: detectPackageManager(repositoryRoot),
|
|
51
|
+
workspaceManager: detectWorkspaceManager(repositoryRoot),
|
|
52
|
+
},
|
|
53
|
+
environment: {
|
|
54
|
+
os: getManifestOs(process.platform),
|
|
55
|
+
shell: process.env.SHELL || null,
|
|
56
|
+
nodeVersion: process.version,
|
|
57
|
+
requiredEnvVars: [],
|
|
58
|
+
safeEnv: {},
|
|
59
|
+
},
|
|
60
|
+
ciDiscovery: discoverCiFiles(repositoryRoot),
|
|
61
|
+
frameworks: [
|
|
62
|
+
...selected.map(framework => buildFrameworkScaffold(repositoryRoot, framework)),
|
|
63
|
+
...unsupported.map(framework => buildUnsupportedFrameworkScaffold(repositoryRoot, framework)),
|
|
64
|
+
],
|
|
65
|
+
omitted: [],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const errors = validateManifest(manifest)
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
throw new Error(`Generated manifest scaffold is invalid:\n- ${errors.join('\n- ')}`)
|
|
71
|
+
}
|
|
72
|
+
return manifest
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds a diagnostic-only entry for a detected runner the validator cannot execute.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} repositoryRoot repository root
|
|
79
|
+
* @param {object} detection static framework detection
|
|
80
|
+
* @returns {object} non-runnable framework manifest entry
|
|
81
|
+
*/
|
|
82
|
+
function buildUnsupportedFrameworkScaffold (repositoryRoot, detection) {
|
|
83
|
+
const packageJsonPath = getDetectionPackageJson(repositoryRoot, detection.locations)
|
|
84
|
+
const projectRoot = path.dirname(packageJsonPath)
|
|
85
|
+
const packageJson = readJson(packageJsonPath) || {}
|
|
86
|
+
const framework = detection.id === 'node-test' ? 'node:test' : detection.id
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
id: `${detection.id}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
|
|
90
|
+
framework,
|
|
91
|
+
frameworkVersion: getInstalledFrameworkVersion(detection.id, projectRoot, packageJson),
|
|
92
|
+
language: 'unknown',
|
|
93
|
+
status: 'unsupported_by_validator',
|
|
94
|
+
supportLevel: 'detected_only',
|
|
95
|
+
project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
|
|
96
|
+
notes: [
|
|
97
|
+
`${detection.name} was detected at ${detection.locations.join(', ') || 'an unknown location'}, but is not ` +
|
|
98
|
+
'supported by this Test Optimization validator.',
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildFrameworkScaffold (repositoryRoot, detection) {
|
|
104
|
+
const packageJsonPath = path.resolve(repositoryRoot, detection.commandLocation || 'package.json')
|
|
105
|
+
const projectRoot = path.dirname(packageJsonPath)
|
|
106
|
+
const packageJson = readJson(packageJsonPath) || {}
|
|
107
|
+
const framework = detection.id
|
|
108
|
+
|
|
109
|
+
if (!SUPPORTED_SCAFFOLD_FRAMEWORKS.has(framework)) {
|
|
110
|
+
return {
|
|
111
|
+
id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
|
|
112
|
+
framework,
|
|
113
|
+
frameworkVersion: detection.version,
|
|
114
|
+
status: 'detected_not_runnable',
|
|
115
|
+
project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
|
|
116
|
+
notes: [`${detection.name} was detected, but automatic generated-test scaffolding is not available.`],
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const runner = tryResolveRunner(framework, projectRoot)
|
|
121
|
+
if (!runner) {
|
|
122
|
+
return {
|
|
123
|
+
id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
|
|
124
|
+
framework,
|
|
125
|
+
frameworkVersion: detection.version,
|
|
126
|
+
status: 'requires_manual_setup',
|
|
127
|
+
project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
|
|
128
|
+
notes: [
|
|
129
|
+
`${detection.name} was detected, but its executable package could not be resolved from ` +
|
|
130
|
+
`${path.relative(repositoryRoot, projectRoot) || 'the repository root'}.`,
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const scriptName = getPackageScriptName(packageJson, detection.command)
|
|
135
|
+
const preserveProjectWrapper = Boolean(scriptName) && !usesBareFrameworkRunner(detection.command, framework)
|
|
136
|
+
const baseCommand = buildExistingCommand({
|
|
137
|
+
framework,
|
|
138
|
+
projectRoot,
|
|
139
|
+
repositoryRoot,
|
|
140
|
+
runner,
|
|
141
|
+
scriptName,
|
|
142
|
+
preserveProjectWrapper,
|
|
143
|
+
})
|
|
144
|
+
const representative = findRepresentativeTestFile(projectRoot)
|
|
145
|
+
const command = representative
|
|
146
|
+
? buildFocusedCommand(baseCommand, framework, representative, Boolean(scriptName), preserveProjectWrapper)
|
|
147
|
+
: baseCommand
|
|
148
|
+
|
|
149
|
+
const generatedTestStrategy = buildGeneratedTestStrategy({
|
|
150
|
+
baseCommand: preserveProjectWrapper ? baseCommand : undefined,
|
|
151
|
+
framework,
|
|
152
|
+
packageJson,
|
|
153
|
+
projectRoot,
|
|
154
|
+
representative,
|
|
155
|
+
runner,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
|
|
160
|
+
framework,
|
|
161
|
+
frameworkVersion: detection.version,
|
|
162
|
+
language: /\.tsx?$/.test(generatedTestStrategy.fileExtension) ? 'typescript' : 'javascript',
|
|
163
|
+
status: 'runnable',
|
|
164
|
+
supportLevel: 'validator_supported',
|
|
165
|
+
project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
|
|
166
|
+
setup: { commands: [], services: [] },
|
|
167
|
+
existingTestCommand: command,
|
|
168
|
+
preflight: { status: 'pending', maxTestCount: 50 },
|
|
169
|
+
ciWiring: {
|
|
170
|
+
status: 'unknown',
|
|
171
|
+
replayability: 'not_replayable',
|
|
172
|
+
replayBlocker: 'CI command selection has not been completed. Inspect the discovered CI configuration and ' +
|
|
173
|
+
'replace this with a concrete technical blocker only when the selected test command cannot be replayed.',
|
|
174
|
+
diagnosis: 'Select one replayable CI test step and record its exact command and environment before live CI ' +
|
|
175
|
+
'wiring validation.',
|
|
176
|
+
initialization: {
|
|
177
|
+
status: 'unknown',
|
|
178
|
+
evidence: [],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
generatedTestStrategy,
|
|
182
|
+
notes: [
|
|
183
|
+
representative
|
|
184
|
+
? `Generated by --init-manifest using representative test ${path.relative(repositoryRoot, representative)}.`
|
|
185
|
+
: 'Generated by --init-manifest. Narrow existingTestCommand if the detected command runs a broad suite.',
|
|
186
|
+
preserveProjectWrapper
|
|
187
|
+
? `Basic Reporting preserves package script ${scriptName} because it uses a custom test wrapper.`
|
|
188
|
+
: `Basic Reporting invokes the installed ${framework} runner directly; record the CI wrapper separately.`,
|
|
189
|
+
'CI command selection still requires repository-specific evidence.',
|
|
190
|
+
],
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getProject ({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }) {
|
|
195
|
+
return {
|
|
196
|
+
name: packageJson.name || getProjectIdentifier(packageJson, projectRoot, repositoryRoot),
|
|
197
|
+
root: projectRoot,
|
|
198
|
+
packageJson: packageJsonPath,
|
|
199
|
+
configFiles: findConfigFiles(projectRoot, framework),
|
|
200
|
+
evidence: [`Detected ${framework} from ${path.relative(repositoryRoot, packageJsonPath) || 'package.json'}.`],
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildExistingCommand ({
|
|
205
|
+
framework,
|
|
206
|
+
projectRoot,
|
|
207
|
+
repositoryRoot,
|
|
208
|
+
runner,
|
|
209
|
+
scriptName,
|
|
210
|
+
preserveProjectWrapper,
|
|
211
|
+
}) {
|
|
212
|
+
const packageManager = preserveProjectWrapper ? detectPackageManager(repositoryRoot) : undefined
|
|
213
|
+
const argv = preserveProjectWrapper
|
|
214
|
+
? getPackageScriptArgv(packageManager, scriptName, repositoryRoot)
|
|
215
|
+
: getDirectRunnerArgv(framework, runner)
|
|
216
|
+
return {
|
|
217
|
+
description: preserveProjectWrapper
|
|
218
|
+
? `Detected custom package script ${scriptName}`
|
|
219
|
+
: `Direct installed ${framework} runner for local capability validation`,
|
|
220
|
+
cwd: projectRoot,
|
|
221
|
+
argv,
|
|
222
|
+
env: {},
|
|
223
|
+
requiredEnvVars: [],
|
|
224
|
+
timeoutMs: 300_000,
|
|
225
|
+
usesShell: false,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildGeneratedTestStrategy ({ baseCommand, framework, packageJson, projectRoot, representative, runner }) {
|
|
230
|
+
const convention = getGeneratedTestConvention(representative, projectRoot)
|
|
231
|
+
const packageType = getNearestPackageType(convention.testDirectory, projectRoot, packageJson.type)
|
|
232
|
+
const moduleSystem = getGeneratedModuleSystem(framework, convention.fileExtension, packageType)
|
|
233
|
+
const definitions = getGeneratedDefinitions({ framework, convention, moduleSystem })
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
status: 'planned',
|
|
237
|
+
reason: 'Standard isolated scenarios generated by the validator manifest scaffold.',
|
|
238
|
+
adapter: framework,
|
|
239
|
+
testDirectory: convention.testDirectory,
|
|
240
|
+
moduleSystem,
|
|
241
|
+
fileExtension: convention.fileExtension,
|
|
242
|
+
supportsFocusedSingleFileRun: true,
|
|
243
|
+
usesMultipleFiles: true,
|
|
244
|
+
files: definitions.map(definition => ({
|
|
245
|
+
path: definition.file,
|
|
246
|
+
role: 'test',
|
|
247
|
+
contentLines: definition.content.split('\n'),
|
|
248
|
+
})),
|
|
249
|
+
scenarios: definitions.map(definition => ({
|
|
250
|
+
id: definition.id,
|
|
251
|
+
purpose: definition.purpose,
|
|
252
|
+
runCommand: baseCommand
|
|
253
|
+
? buildFocusedCommand(baseCommand, framework, definition.file, true, true, moduleSystem)
|
|
254
|
+
: buildGeneratedRunCommand(framework, projectRoot, definition.file, runner, moduleSystem),
|
|
255
|
+
expectedWithoutDatadog: {
|
|
256
|
+
exitCode: definition.id === 'atr-fail-once' ? 1 : 0,
|
|
257
|
+
observedTestCount: 1,
|
|
258
|
+
},
|
|
259
|
+
testIdentities: [{
|
|
260
|
+
suite: null,
|
|
261
|
+
name: definition.testName,
|
|
262
|
+
file: definition.file,
|
|
263
|
+
parameters: null,
|
|
264
|
+
}],
|
|
265
|
+
})),
|
|
266
|
+
cleanupPaths: [
|
|
267
|
+
...definitions.map(definition => definition.file),
|
|
268
|
+
path.join(path.dirname(definitions.find(definition => definition.id === 'atr-fail-once').file),
|
|
269
|
+
'.dd-test-optimization-validation-atr-state'),
|
|
270
|
+
],
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getGeneratedModuleSystem (framework, fileExtension, packageType) {
|
|
275
|
+
if (/\.(?:cjs|cts)$/.test(fileExtension)) return 'commonjs'
|
|
276
|
+
if (framework === 'vitest' || /\.(?:mjs|mts)$/.test(fileExtension)) return 'esm'
|
|
277
|
+
return packageType === 'module' ? 'esm' : 'commonjs'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Returns the package module type that applies to a generated test directory.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} testDirectory generated test directory
|
|
284
|
+
* @param {string} projectRoot detected project root
|
|
285
|
+
* @param {string|undefined} fallbackType detected project package type
|
|
286
|
+
* @returns {string|undefined} nearest package module type
|
|
287
|
+
*/
|
|
288
|
+
function getNearestPackageType (testDirectory, projectRoot, fallbackType) {
|
|
289
|
+
const root = path.resolve(projectRoot)
|
|
290
|
+
let directory = path.resolve(testDirectory)
|
|
291
|
+
|
|
292
|
+
while (directory === root || isPathInside(root, directory)) {
|
|
293
|
+
const packageJson = readJson(path.join(directory, 'package.json'))
|
|
294
|
+
if (typeof packageJson?.type === 'string') return packageJson.type
|
|
295
|
+
if (directory === root) break
|
|
296
|
+
directory = path.dirname(directory)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return fallbackType
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getGeneratedTestConvention (representative, projectRoot) {
|
|
303
|
+
if (!representative) {
|
|
304
|
+
return {
|
|
305
|
+
exactFilename: undefined,
|
|
306
|
+
fileExtension: '.test.js',
|
|
307
|
+
testDirectory: path.join(projectRoot, 'test'),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const basename = path.basename(representative)
|
|
312
|
+
if (/^test\.[cm]?[jt]s$/.test(basename)) {
|
|
313
|
+
const representativeDirectory = path.dirname(representative)
|
|
314
|
+
return {
|
|
315
|
+
exactFilename: basename,
|
|
316
|
+
fileExtension: path.extname(basename),
|
|
317
|
+
testDirectory: representativeDirectory === projectRoot
|
|
318
|
+
? projectRoot
|
|
319
|
+
: path.dirname(representativeDirectory),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
exactFilename: undefined,
|
|
325
|
+
fileExtension: getTestExtension(representative),
|
|
326
|
+
testDirectory: path.dirname(representative),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getGeneratedDefinitions ({ framework, convention, moduleSystem }) {
|
|
331
|
+
const stateFileExpression = moduleSystem === 'esm'
|
|
332
|
+
? "join(dirname(fileURLToPath(import.meta.url)), '.dd-test-optimization-validation-atr-state')"
|
|
333
|
+
: "path.join(__dirname, '.dd-test-optimization-validation-atr-state')"
|
|
334
|
+
const definitions = [
|
|
335
|
+
{ id: 'basic-pass', purpose: 'basic_reporting|efd_candidate', testName: 'basic-pass' },
|
|
336
|
+
{ id: 'atr-fail-once', purpose: 'auto_test_retries_candidate', testName: 'atr-fail-once' },
|
|
337
|
+
{ id: 'test-management-target', purpose: 'test_management_candidate', testName: 'test-management-target' },
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
return definitions.map(definition => {
|
|
341
|
+
const prefix = `dd-test-optimization-validation-${definition.id}`
|
|
342
|
+
const filename = convention.exactFilename
|
|
343
|
+
? path.join(prefix, convention.exactFilename)
|
|
344
|
+
: `${prefix}${convention.fileExtension}`
|
|
345
|
+
return {
|
|
346
|
+
...definition,
|
|
347
|
+
file: path.join(convention.testDirectory, filename),
|
|
348
|
+
content: getGeneratedTestContent({ framework, definition, moduleSystem, stateFileExpression }),
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function getGeneratedTestContent ({ framework, definition, moduleSystem, stateFileExpression }) {
|
|
354
|
+
const imports = []
|
|
355
|
+
if (framework === 'vitest' && moduleSystem === 'esm') {
|
|
356
|
+
imports.push("import { describe, expect, it } from 'vitest'")
|
|
357
|
+
}
|
|
358
|
+
if (framework === 'mocha') {
|
|
359
|
+
imports.push(moduleSystem === 'esm'
|
|
360
|
+
? "import assert from 'node:assert/strict'"
|
|
361
|
+
: "const assert = require('node:assert/strict')")
|
|
362
|
+
}
|
|
363
|
+
if (definition.id === 'atr-fail-once') {
|
|
364
|
+
if (moduleSystem === 'esm') {
|
|
365
|
+
imports.push(
|
|
366
|
+
"import { existsSync, writeFileSync } from 'node:fs'",
|
|
367
|
+
"import { dirname, join } from 'node:path'",
|
|
368
|
+
"import { fileURLToPath } from 'node:url'"
|
|
369
|
+
)
|
|
370
|
+
} else {
|
|
371
|
+
imports.push("const fs = require('node:fs')", "const path = require('node:path')")
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const assertion = framework === 'mocha' ? 'assert.equal(1, 1)' : 'expect(true).toBe(true)'
|
|
375
|
+
const testFunction = framework === 'jest' ? 'test' : 'it'
|
|
376
|
+
const body = definition.id === 'atr-fail-once'
|
|
377
|
+
? getAtrBody({ moduleSystem, stateFileExpression, assertion })
|
|
378
|
+
: ` ${assertion}`
|
|
379
|
+
|
|
380
|
+
return [
|
|
381
|
+
...imports,
|
|
382
|
+
imports.length > 0 ? '' : undefined,
|
|
383
|
+
"describe('dd-test-optimization-validation', () => {",
|
|
384
|
+
` ${testFunction}('${definition.testName}', () => {`,
|
|
385
|
+
body,
|
|
386
|
+
' })',
|
|
387
|
+
'})',
|
|
388
|
+
].filter(line => line !== undefined).join('\n')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getAtrBody ({ moduleSystem, stateFileExpression, assertion }) {
|
|
392
|
+
const exists = moduleSystem === 'esm' ? 'existsSync' : 'fs.existsSync'
|
|
393
|
+
const write = moduleSystem === 'esm' ? 'writeFileSync' : 'fs.writeFileSync'
|
|
394
|
+
return [
|
|
395
|
+
` const stateFile = ${stateFileExpression}`,
|
|
396
|
+
` if (!${exists}(stateFile)) {`,
|
|
397
|
+
` ${write}(stateFile, 'failed-once')`,
|
|
398
|
+
" throw new Error('dd-test-optimization-validation atr first failure')",
|
|
399
|
+
' }',
|
|
400
|
+
` ${assertion}`,
|
|
401
|
+
].join('\n')
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function buildGeneratedRunCommand (framework, projectRoot, filename, runner, moduleSystem) {
|
|
405
|
+
const args = {
|
|
406
|
+
jest: ['--runTestsByPath', filename, '--runInBand', '--silent', '--no-watchman'],
|
|
407
|
+
mocha: ['--reporter', 'spec', filename],
|
|
408
|
+
vitest: ['run', filename, ...(moduleSystem === 'commonjs' ? ['--globals'] : [])],
|
|
409
|
+
}[framework]
|
|
410
|
+
return {
|
|
411
|
+
cwd: projectRoot,
|
|
412
|
+
argv: [process.execPath, runner, ...args],
|
|
413
|
+
env: {},
|
|
414
|
+
requiredEnvVars: [],
|
|
415
|
+
timeoutMs: 300_000,
|
|
416
|
+
usesShell: false,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Adds a single test file selection to a detected project command.
|
|
422
|
+
*
|
|
423
|
+
* @param {object} baseCommand detected project command
|
|
424
|
+
* @param {string} framework detected test framework
|
|
425
|
+
* @param {string} filename selected test file
|
|
426
|
+
* @param {boolean} packageScript whether the command invokes a package script
|
|
427
|
+
* @param {boolean} preserveDefaultReporter whether a repository wrapper owns reporter selection
|
|
428
|
+
* @param {string} [moduleSystem] generated test module system
|
|
429
|
+
* @returns {object} focused project command
|
|
430
|
+
*/
|
|
431
|
+
function buildFocusedCommand (
|
|
432
|
+
baseCommand,
|
|
433
|
+
framework,
|
|
434
|
+
filename,
|
|
435
|
+
packageScript,
|
|
436
|
+
preserveDefaultReporter,
|
|
437
|
+
moduleSystem
|
|
438
|
+
) {
|
|
439
|
+
const argv = [...baseCommand.argv]
|
|
440
|
+
if (packageScript && path.basename(argv[0]).toLowerCase() === 'npm') argv.push('--')
|
|
441
|
+
argv.push(...getFocusedTestArgs(framework, filename, preserveDefaultReporter, moduleSystem))
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
...baseCommand,
|
|
445
|
+
description: `${baseCommand.description} targeting ${path.basename(filename)}`,
|
|
446
|
+
argv,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Returns framework arguments that select exactly one test file.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} framework detected test framework
|
|
454
|
+
* @param {string} filename selected test file
|
|
455
|
+
* @param {boolean} preserveDefaultReporter whether a repository wrapper owns reporter selection
|
|
456
|
+
* @param {string} [moduleSystem] generated test module system
|
|
457
|
+
* @returns {string[]} focused test arguments
|
|
458
|
+
*/
|
|
459
|
+
function getFocusedTestArgs (framework, filename, preserveDefaultReporter, moduleSystem) {
|
|
460
|
+
if (framework === 'jest') {
|
|
461
|
+
return [
|
|
462
|
+
'--runTestsByPath',
|
|
463
|
+
filename,
|
|
464
|
+
'--runInBand',
|
|
465
|
+
...(preserveDefaultReporter ? [] : ['--silent']),
|
|
466
|
+
'--no-watchman',
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
return [filename, ...(framework === 'vitest' && moduleSystem === 'commonjs' ? ['--globals'] : [])]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resolves an installed framework executable without making the whole scaffold fail when a nested package only
|
|
474
|
+
* declares the dependency.
|
|
475
|
+
*
|
|
476
|
+
* @param {string} framework detected framework
|
|
477
|
+
* @param {string} projectRoot detected project root
|
|
478
|
+
* @returns {string|undefined} resolved executable path
|
|
479
|
+
*/
|
|
480
|
+
function tryResolveRunner (framework, projectRoot) {
|
|
481
|
+
try {
|
|
482
|
+
return resolveRunner(framework, projectRoot)
|
|
483
|
+
} catch {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function resolveRunner (framework, projectRoot) {
|
|
487
|
+
const packageName = framework === 'jest' ? 'jest' : framework
|
|
488
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`, { paths: [projectRoot] })
|
|
489
|
+
const packageJson = readJson(packageJsonPath)
|
|
490
|
+
const bin = typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin[packageName]
|
|
491
|
+
return path.resolve(path.dirname(packageJsonPath), bin)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getPackageScriptArgv (packageManager, scriptName, repositoryRoot) {
|
|
495
|
+
if (packageManager === 'yarn') {
|
|
496
|
+
const release = findYarnRelease(repositoryRoot)
|
|
497
|
+
return release ? [process.execPath, release, 'run', scriptName] : ['yarn', 'run', scriptName]
|
|
498
|
+
}
|
|
499
|
+
return [packageManager, 'run', scriptName]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Finds the package script whose value produced the detected framework command.
|
|
504
|
+
*
|
|
505
|
+
* @param {object} packageJson project package metadata
|
|
506
|
+
* @param {string} command detected framework command
|
|
507
|
+
* @returns {string|undefined} package script name
|
|
508
|
+
*/
|
|
509
|
+
function getPackageScriptName (packageJson, command) {
|
|
510
|
+
return Object.entries(packageJson.scripts || {}).find(([, value]) => value === command)?.[0]
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Reports whether a package script invokes the framework binary without a repository wrapper.
|
|
515
|
+
*
|
|
516
|
+
* @param {string} command detected framework command
|
|
517
|
+
* @param {string} framework detected test framework
|
|
518
|
+
* @returns {boolean} whether the command starts with the framework binary
|
|
519
|
+
*/
|
|
520
|
+
function usesBareFrameworkRunner (command, framework) {
|
|
521
|
+
const tokens = String(command || '').trim().split(/\s+/)
|
|
522
|
+
const executable = path.basename(tokens[0] || '').replace(/\.cmd$/i, '').toLowerCase()
|
|
523
|
+
if (executable !== framework) return false
|
|
524
|
+
return tokens.length === 1 || framework === 'vitest' && tokens.length === 2 && tokens[1] === 'run'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getDirectRunnerArgv (framework, runner) {
|
|
528
|
+
return [process.execPath, runner, ...(framework === 'vitest' ? ['run'] : [])]
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function findRepresentativeTestFile (root) {
|
|
532
|
+
const stack = [root]
|
|
533
|
+
const candidates = []
|
|
534
|
+
const packageRanks = new Map()
|
|
535
|
+
let visited = 0
|
|
536
|
+
while (stack.length > 0 && visited < 5000) {
|
|
537
|
+
const directory = stack.pop()
|
|
538
|
+
let entries
|
|
539
|
+
try {
|
|
540
|
+
entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
541
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
542
|
+
} catch {
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
for (const entry of entries) {
|
|
546
|
+
if (['.git', 'node_modules', 'coverage', 'dist', 'build'].includes(entry.name)) continue
|
|
547
|
+
const filename = path.join(directory, entry.name)
|
|
548
|
+
if (entry.isDirectory()) {
|
|
549
|
+
stack.push(filename)
|
|
550
|
+
continue
|
|
551
|
+
}
|
|
552
|
+
visited++
|
|
553
|
+
const inTestsDirectory = path.relative(root, directory).split(path.sep).includes('__tests__')
|
|
554
|
+
if (/^(?:test\.(?:[cm]?[jt]s|[jt]sx)|.+[.-](?:test|spec)\.(?:[cm]?[jt]s|[jt]sx))$/.test(entry.name) ||
|
|
555
|
+
(inTestsDirectory && /\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name))) {
|
|
556
|
+
candidates.push(filename)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
candidates.sort((left, right) => {
|
|
562
|
+
return getTestDirectoryRank(left, root) - getTestDirectoryRank(right, root) ||
|
|
563
|
+
getTestAreaRank(left, root) - getTestAreaRank(right, root) ||
|
|
564
|
+
getIndependentTestProjectRank(left, root, packageRanks) -
|
|
565
|
+
getIndependentTestProjectRank(right, root, packageRanks) ||
|
|
566
|
+
left.localeCompare(right)
|
|
567
|
+
})
|
|
568
|
+
return candidates[0]
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Ranks established test directories ahead of source-adjacent test-looking files.
|
|
573
|
+
*
|
|
574
|
+
* @param {string} filename candidate test file
|
|
575
|
+
* @param {string} root detected project root
|
|
576
|
+
* @returns {number} directory preference rank
|
|
577
|
+
*/
|
|
578
|
+
function getTestDirectoryRank (filename, root) {
|
|
579
|
+
const directories = path.relative(root, path.dirname(filename)).split(path.sep)
|
|
580
|
+
if (directories.includes('__tests__')) return 0
|
|
581
|
+
if (directories.some(directory => directory === 'test' || directory === 'tests')) return 1
|
|
582
|
+
return 2
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Ranks conventional project areas ahead of auxiliary repository trees.
|
|
587
|
+
*
|
|
588
|
+
* @param {string} filename candidate test file
|
|
589
|
+
* @param {string} root detected project root
|
|
590
|
+
* @returns {number} project area preference rank
|
|
591
|
+
*/
|
|
592
|
+
function getTestAreaRank (filename, root) {
|
|
593
|
+
const [topLevelDirectory] = path.relative(root, filename).split(path.sep)
|
|
594
|
+
return ['packages', 'src', 'test', 'tests'].includes(topLevelDirectory) ? 0 : 1
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Ranks independently tested nested packages behind tests owned by the detected root command.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} filename candidate test file
|
|
601
|
+
* @param {string} root detected project root
|
|
602
|
+
* @param {Map<string, number>} cache package-directory rank cache
|
|
603
|
+
* @returns {number} independent test project rank
|
|
604
|
+
*/
|
|
605
|
+
function getIndependentTestProjectRank (filename, root, cache) {
|
|
606
|
+
let directory = path.dirname(filename)
|
|
607
|
+
while (directory !== root && directory.startsWith(`${root}${path.sep}`)) {
|
|
608
|
+
if (cache.has(directory)) return cache.get(directory)
|
|
609
|
+
|
|
610
|
+
const packageJson = readJson(path.join(directory, 'package.json'))
|
|
611
|
+
if (packageJson) {
|
|
612
|
+
const rank = typeof packageJson.scripts?.test === 'string' ? 1 : 0
|
|
613
|
+
cache.set(directory, rank)
|
|
614
|
+
return rank
|
|
615
|
+
}
|
|
616
|
+
directory = path.dirname(directory)
|
|
617
|
+
}
|
|
618
|
+
return 0
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function getTestExtension (filename) {
|
|
622
|
+
const match = /((?:\.test|\.spec)\.(?:[cm]?[jt]s|[jt]sx))$/.exec(filename)
|
|
623
|
+
return match?.[1] || '.test.js'
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function findConfigFiles (root, framework) {
|
|
627
|
+
const patterns = {
|
|
628
|
+
jest: /^jest\.config\./,
|
|
629
|
+
mocha: /^\.mocharc\./,
|
|
630
|
+
vitest: /^(?:vite|vitest)\.config\./,
|
|
631
|
+
}[framework]
|
|
632
|
+
if (!patterns) return []
|
|
633
|
+
try {
|
|
634
|
+
return fs.readdirSync(root).filter(filename => patterns.test(filename)).map(filename => path.join(root, filename))
|
|
635
|
+
} catch {
|
|
636
|
+
return []
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Resolves the package.json associated with a framework detection.
|
|
642
|
+
*
|
|
643
|
+
* @param {string} repositoryRoot repository root
|
|
644
|
+
* @param {string[]} locations detected evidence paths
|
|
645
|
+
* @returns {string} absolute package.json path
|
|
646
|
+
*/
|
|
647
|
+
function getDetectionPackageJson (repositoryRoot, locations = []) {
|
|
648
|
+
const location = locations.find(value => path.basename(value) === 'package.json')
|
|
649
|
+
return path.resolve(repositoryRoot, location || 'package.json')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Resolves a detected runner version without executing repository code.
|
|
654
|
+
*
|
|
655
|
+
* @param {string} framework detected framework package name
|
|
656
|
+
* @param {string} projectRoot detected project root
|
|
657
|
+
* @param {object} packageJson detected project package.json
|
|
658
|
+
* @returns {string|null} installed or declared framework version
|
|
659
|
+
*/
|
|
660
|
+
function getInstalledFrameworkVersion (framework, projectRoot, packageJson) {
|
|
661
|
+
if (framework === 'node-test') return process.version
|
|
662
|
+
try {
|
|
663
|
+
const filename = require.resolve(`${framework}/package.json`, { paths: [projectRoot] })
|
|
664
|
+
return readJson(filename)?.version || null
|
|
665
|
+
} catch {}
|
|
666
|
+
|
|
667
|
+
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
668
|
+
if (typeof packageJson[field]?.[framework] === 'string') return packageJson[field][framework]
|
|
669
|
+
}
|
|
670
|
+
return null
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function discoverCiFiles (root) {
|
|
674
|
+
const found = []
|
|
675
|
+
for (const relativePath of CI_PATHS) {
|
|
676
|
+
const filename = path.join(root, relativePath)
|
|
677
|
+
if (!fs.existsSync(filename)) continue
|
|
678
|
+
const stat = fs.statSync(filename)
|
|
679
|
+
if (stat.isDirectory()) {
|
|
680
|
+
for (const entry of fs.readdirSync(filename).sort()) {
|
|
681
|
+
if (/\.ya?ml$/.test(entry)) found.push(path.posix.join(relativePath, entry))
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
found.push(relativePath)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
searched: [...CI_PATHS],
|
|
689
|
+
found,
|
|
690
|
+
method: 'deterministic-known-ci-paths',
|
|
691
|
+
warnings: [],
|
|
692
|
+
notes: ['Generated by --init-manifest; select and record one replayable CI test step before live validation.'],
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function detectPackageManager (root) {
|
|
697
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
698
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn'
|
|
699
|
+
return 'npm'
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function detectWorkspaceManager (root) {
|
|
703
|
+
if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))) return 'pnpm'
|
|
704
|
+
const packageJson = readJson(path.join(root, 'package.json'))
|
|
705
|
+
return packageJson?.workspaces ? detectPackageManager(root) : 'none'
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function getManifestOs (platform) {
|
|
709
|
+
if (platform === 'win32') return 'windows'
|
|
710
|
+
if (platform === 'darwin' || platform === 'linux') return platform
|
|
711
|
+
return 'unknown'
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function findYarnRelease (root) {
|
|
715
|
+
const directory = path.join(root, '.yarn', 'releases')
|
|
716
|
+
try {
|
|
717
|
+
const release = fs.readdirSync(directory).find(filename => /^yarn-.+\.cjs$/.test(filename))
|
|
718
|
+
return release && path.join(directory, release)
|
|
719
|
+
} catch {}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function getProjectIdentifier (packageJson, projectRoot, repositoryRoot) {
|
|
723
|
+
if (packageJson.name) return packageJson.name.replaceAll(/[^A-Za-z0-9._-]+/g, '-')
|
|
724
|
+
return (path.relative(repositoryRoot, projectRoot) || 'root').replaceAll(path.sep, '-')
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function isPathInside (root, filename) {
|
|
728
|
+
const relative = path.relative(root, filename)
|
|
729
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function readJson (filename) {
|
|
733
|
+
try {
|
|
734
|
+
return JSON.parse(fs.readFileSync(filename, 'utf8'))
|
|
735
|
+
} catch {}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
module.exports = { createManifestScaffold }
|