dd-trace 6.6.0 → 6.8.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/ci/diagnose.js +162 -21
- package/ci/init.js +0 -1
- package/ci/runbook.md +187 -180
- package/ci/test-optimization-validation/approval-artifacts.js +3 -1
- package/ci/test-optimization-validation/approval.js +52 -15
- package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
- package/ci/test-optimization-validation/ci-discovery.js +1 -1
- package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
- package/ci/test-optimization-validation/ci-remediation.js +112 -39
- package/ci/test-optimization-validation/cli.js +516 -681
- package/ci/test-optimization-validation/command-blocker.js +188 -33
- package/ci/test-optimization-validation/command-output-policy.js +8 -31
- package/ci/test-optimization-validation/command-runner.js +189 -245
- package/ci/test-optimization-validation/environment.js +90 -0
- package/ci/test-optimization-validation/executable.js +159 -388
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
- package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
- package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
- package/ci/test-optimization-validation/generated-files.js +84 -22
- package/ci/test-optimization-validation/generated-test-contract.js +281 -0
- package/ci/test-optimization-validation/generated-verifier.js +49 -16
- package/ci/test-optimization-validation/literal-environment.js +57 -0
- package/ci/test-optimization-validation/manifest-loader.js +2 -22
- package/ci/test-optimization-validation/manifest-scaffold.js +815 -515
- package/ci/test-optimization-validation/manifest-schema.js +494 -702
- package/ci/test-optimization-validation/offline-fixtures.js +4 -1
- package/ci/test-optimization-validation/offline-output.js +6 -6
- package/ci/test-optimization-validation/plan-writer.js +243 -984
- package/ci/test-optimization-validation/preflight-runner.js +49 -55
- package/ci/test-optimization-validation/redaction.js +2 -1
- package/ci/test-optimization-validation/report-writer.js +391 -1357
- package/ci/test-optimization-validation/result-semantics.js +86 -0
- package/ci/test-optimization-validation/runner-command.js +249 -0
- package/ci/test-optimization-validation/runner-contract.js +664 -0
- package/ci/test-optimization-validation/safe-files.js +4 -4
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
- package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
- package/ci/test-optimization-validation/scenarios/test-management.js +6 -5
- package/ci/test-optimization-validation/source-text.js +87 -0
- package/ci/test-optimization-validation/test-output.js +12 -22
- package/ci/vitest-no-worker-init-setup.mjs +348 -51
- package/ext/tags.d.ts +1 -0
- package/index.d.ts +84 -13
- package/package.json +10 -4
- package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
- package/packages/datadog-esbuild/index.js +4 -2
- package/packages/datadog-esbuild/src/utils.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +4 -4
- package/packages/datadog-instrumentations/src/cucumber.js +9 -6
- package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +57 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
- package/packages/datadog-instrumentations/src/http/client.js +1 -3
- package/packages/datadog-instrumentations/src/jest.js +8 -14
- package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
- package/packages/datadog-instrumentations/src/mocha/main.js +2 -42
- package/packages/datadog-instrumentations/src/mocha/utils.js +68 -2
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +19 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +446 -42
- package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
- package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
- package/packages/datadog-instrumentations/src/playwright.js +3 -2
- package/packages/datadog-instrumentations/src/rhea.js +1 -1
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +525 -56
- package/packages/datadog-instrumentations/src/vitest-main.js +90 -10
- package/packages/datadog-instrumentations/src/webdriverio.js +914 -0
- package/packages/datadog-plugin-aerospike/src/index.js +1 -3
- package/packages/datadog-plugin-amqplib/src/client.js +1 -1
- package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
- package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
- package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
- package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
- package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
- package/packages/datadog-plugin-child_process/src/index.js +3 -3
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +5 -8
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
- package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
- package/packages/datadog-plugin-fs/src/index.js +0 -4
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
- package/packages/datadog-plugin-graphql/src/execute.js +1 -0
- package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
- package/packages/datadog-plugin-http/src/client.js +2 -1
- package/packages/datadog-plugin-jest/src/index.js +2 -3
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-mocha/src/index.js +25 -6
- package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
- package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
- package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
- package/packages/datadog-plugin-prisma/src/index.js +5 -1
- package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
- package/packages/datadog-plugin-rhea/src/producer.js +1 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -1
- package/packages/datadog-plugin-vitest/src/index.js +84 -11
- package/packages/datadog-webpack/index.js +2 -2
- package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
- package/packages/dd-trace/src/appsec/sdk/track_event.js +1 -1
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +50 -51
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -1
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +4 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +7 -5
- package/packages/dd-trace/src/config/defaults.js +14 -6
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
- package/packages/dd-trace/src/config/index.js +15 -0
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/supported-configurations.json +12 -6
- package/packages/dd-trace/src/datastreams/encoding.js +3 -4
- package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
- package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
- package/packages/dd-trace/src/encode/span-stats.js +7 -1
- package/packages/dd-trace/src/encode/tags-processors.js +2 -2
- package/packages/dd-trace/src/exporter.js +16 -9
- package/packages/dd-trace/src/exporters/common/url.js +3 -5
- package/packages/dd-trace/src/exporters/electron/index.js +4 -1
- package/packages/dd-trace/src/external-logger/src/index.js +1 -2
- package/packages/dd-trace/src/flare/index.js +2 -2
- package/packages/dd-trace/src/id.js +1 -1
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
- package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
- package/packages/dd-trace/src/llmobs/experiments/index.js +51 -12
- package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
- package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
- package/packages/dd-trace/src/llmobs/experiments/util.js +218 -0
- package/packages/dd-trace/src/llmobs/index.js +9 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
- package/packages/dd-trace/src/llmobs/sdk.js +6 -12
- package/packages/dd-trace/src/llmobs/span_processor.js +14 -7
- package/packages/dd-trace/src/llmobs/tagger.js +24 -5
- package/packages/dd-trace/src/llmobs/util.js +56 -3
- package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
- package/packages/dd-trace/src/noop/proxy.js +2 -2
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
- package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
- package/packages/dd-trace/src/opentracing/span.js +1 -1
- package/packages/dd-trace/src/opentracing/span_context.js +1 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +114 -23
- package/packages/dd-trace/src/plugins/index.js +2 -0
- package/packages/dd-trace/src/plugins/outbound.js +4 -1
- package/packages/dd-trace/src/plugins/plugin.js +1 -1
- package/packages/dd-trace/src/plugins/tracing.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +7 -6
- package/packages/dd-trace/src/plugins/util/git.js +1 -1
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +1 -1
- package/packages/dd-trace/src/plugins/util/llm.js +2 -2
- package/packages/dd-trace/src/plugins/util/tags.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +38 -12
- package/packages/dd-trace/src/plugins/util/url.js +5 -5
- package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
- package/packages/dd-trace/src/plugins/util/web.js +1 -1
- package/packages/dd-trace/src/priority_sampler.js +6 -3
- package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
- package/packages/dd-trace/src/ritm.js +5 -20
- package/packages/dd-trace/src/span_processor.js +1 -1
- package/packages/dd-trace/src/span_stats.js +22 -4
- package/packages/dd-trace/src/startup-log.js +3 -0
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
- package/packages/dd-trace/src/util.js +16 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/source-map/index.js +1 -1
- package/ci/test-optimization-validation/command-suitability.js +0 -471
- package/ci/test-optimization-validation/init-probe-preload.js +0 -163
- package/ci/test-optimization-validation/init-probe.js +0 -246
- package/ci/test-optimization-validation/late-initialization.js +0 -63
- package/ci/test-optimization-validation/local-command.js +0 -163
- package/ci/test-optimization-validation/setup-runner.js +0 -97
- package/ci/test-optimization-validation-manifest.schema.json +0 -1
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/* eslint-disable eslint-rules/eslint-process-env */
|
|
4
|
-
|
|
5
3
|
const fs = require('node:fs')
|
|
6
4
|
const path = require('node:path')
|
|
7
5
|
|
|
8
6
|
const { runDiagnosis } = require('../diagnose')
|
|
7
|
+
const cucumber = require('./framework-adapters/cucumber')
|
|
8
|
+
const cypress = require('./framework-adapters/cypress')
|
|
9
|
+
const playwright = require('./framework-adapters/playwright')
|
|
10
|
+
const {
|
|
11
|
+
GENERATED_SCENARIOS,
|
|
12
|
+
getGeneratedRetryStatePath,
|
|
13
|
+
getGeneratedTestContent,
|
|
14
|
+
} = require('./generated-test-contract')
|
|
9
15
|
const { validateManifest } = require('./manifest-schema')
|
|
16
|
+
const {
|
|
17
|
+
getProjectNodeRunner,
|
|
18
|
+
getRunnerContract,
|
|
19
|
+
getRunnerSearchRoots,
|
|
20
|
+
} = require('./runner-contract')
|
|
10
21
|
|
|
11
|
-
const
|
|
22
|
+
const SUPPORTED_FRAMEWORKS = new Set(['cucumber', 'cypress', 'jest', 'mocha', 'playwright', 'vitest'])
|
|
23
|
+
const MAX_DISCOVERY_ENTRIES = 5000
|
|
24
|
+
const MAX_DIRECTORY_ENTRIES = 1024
|
|
25
|
+
const MAX_FILE_BYTES = 512 * 1024
|
|
26
|
+
const MAX_CI_FILES = 256
|
|
27
|
+
const MAX_CI_REVIEW_TARGETS = 3
|
|
28
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
29
|
+
'.git',
|
|
30
|
+
'.hg',
|
|
31
|
+
'.svn',
|
|
32
|
+
'.yarn',
|
|
33
|
+
'build',
|
|
34
|
+
'coverage',
|
|
35
|
+
'dd-test-optimization-validation-results',
|
|
36
|
+
'dist',
|
|
37
|
+
'node_modules',
|
|
38
|
+
'out',
|
|
39
|
+
'target',
|
|
40
|
+
'vendor',
|
|
41
|
+
])
|
|
12
42
|
const CI_PATHS = [
|
|
13
43
|
'.github/workflows',
|
|
14
44
|
'.gitlab-ci.yml',
|
|
@@ -18,9 +48,43 @@ const CI_PATHS = [
|
|
|
18
48
|
'azure-pipelines.yml',
|
|
19
49
|
'Jenkinsfile',
|
|
20
50
|
]
|
|
51
|
+
const CONFIG_PATTERNS = {
|
|
52
|
+
cucumber: cucumber.CONFIG_PATTERN,
|
|
53
|
+
cypress: cypress.CONFIG_PATTERN,
|
|
54
|
+
jest: /^(?:jest|config-jest)\.config\.(?:[cm]?[jt]s|json)$/,
|
|
55
|
+
mocha: /^\.mocharc\.(?:json|ya?ml|[cm]?js)$/,
|
|
56
|
+
playwright: playwright.CONFIG_PATTERN,
|
|
57
|
+
vitest: /^(?:vite|vitest)\.config\.[cm]?[jt]s$/,
|
|
58
|
+
}
|
|
59
|
+
const JS_CONFIG_EXTENSIONS = ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts']
|
|
60
|
+
const IMPLICIT_CONFIG_FILENAMES = {
|
|
61
|
+
cucumber: ['json', 'yaml', 'yml', ...JS_CONFIG_EXTENSIONS].map(extension => `cucumber.${extension}`),
|
|
62
|
+
cypress: ['json', ...JS_CONFIG_EXTENSIONS].flatMap(extension => [
|
|
63
|
+
`cypress.${extension}`,
|
|
64
|
+
`cypress.config.${extension}`,
|
|
65
|
+
]),
|
|
66
|
+
jest: ['json', ...JS_CONFIG_EXTENSIONS].flatMap(extension => [
|
|
67
|
+
`jest.config.${extension}`,
|
|
68
|
+
`config-jest.config.${extension}`,
|
|
69
|
+
]),
|
|
70
|
+
mocha: ['json', 'yaml', 'yml', ...JS_CONFIG_EXTENSIONS].map(extension => `.mocharc.${extension}`),
|
|
71
|
+
playwright: JS_CONFIG_EXTENSIONS.map(extension => `playwright.config.${extension}`),
|
|
72
|
+
vitest: JS_CONFIG_EXTENSIONS.flatMap(extension => [
|
|
73
|
+
`vite.config.${extension}`,
|
|
74
|
+
`vitest.config.${extension}`,
|
|
75
|
+
]),
|
|
76
|
+
}
|
|
77
|
+
const TEST_FILE_PATTERN = /^.+[._-](?:test|spec)\.[cm]?[jt]sx?$/
|
|
78
|
+
const BARE_TEST_FILE_PATTERN = /^test\.[cm]?[jt]sx?$/
|
|
79
|
+
const TYPE_ONLY_TEST_PATTERN = /\.(?:test|spec)-d\.[cm]?tsx?$|\.d\.[cm]?ts$/i
|
|
80
|
+
const TYPE_ONLY_DIRECTORY_PATTERN = /(?:^|\/)(?:type[-_]?tests?|typetests?|test-dts?)(?:\/|$)/i
|
|
81
|
+
const LOCAL_SOCKET_PATTERN =
|
|
82
|
+
/(?:\bcreateServer\s*\(|\.listen\s*\(|\b(?:localhost|127\.0\.0\.1)\b|\bcy\.(?:visit|request)\s*\()/
|
|
83
|
+
const CYPRESS_LOCAL_ORIGIN_PATTERN =
|
|
84
|
+
/\bcy\.(?:visit|request)\s*\([\s\S]{0,512}\b(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)(?=[:/'"`\s}])/i
|
|
21
85
|
|
|
22
86
|
/**
|
|
23
|
-
* Creates a schema-valid
|
|
87
|
+
* Creates a schema-valid data-only manifest without executing project code.
|
|
24
88
|
*
|
|
25
89
|
* @param {object} input scaffold inputs
|
|
26
90
|
* @param {string} input.root repository root
|
|
@@ -28,39 +92,37 @@ const CI_PATHS = [
|
|
|
28
92
|
* @returns {object} validation manifest scaffold
|
|
29
93
|
*/
|
|
30
94
|
function createManifestScaffold ({ root, frameworks = new Set() }) {
|
|
31
|
-
const repositoryRoot = path.resolve(root)
|
|
95
|
+
const repositoryRoot = fs.realpathSync(path.resolve(root))
|
|
32
96
|
const diagnosis = runDiagnosis({ root: repositoryRoot, env: {} })
|
|
33
|
-
const
|
|
34
|
-
|
|
97
|
+
const ciDiscovery = discoverCiFiles(repositoryRoot)
|
|
98
|
+
const eligible = diagnosis.eligibleFrameworks.filter(detection => isSelected(detection.id, frameworks))
|
|
99
|
+
const eligibleKinds = new Set(eligible.map(detection => detection.id))
|
|
100
|
+
const detectedOnly = diagnosis.supportedFrameworks.filter(detection => {
|
|
101
|
+
return !eligibleKinds.has(detection.id) && isSelected(detection.id, frameworks)
|
|
35
102
|
})
|
|
36
|
-
const unsupported = diagnosis.unsupportedFrameworks.filter(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (selected.length === 0 && unsupported.length === 0) {
|
|
103
|
+
const unsupported = diagnosis.unsupportedFrameworks.filter(detection => isSelected(detection.id, frameworks))
|
|
104
|
+
|
|
105
|
+
if (eligible.length === 0 && detectedOnly.length === 0 && unsupported.length === 0) {
|
|
40
106
|
throw new Error('No test framework was detected for manifest scaffolding.')
|
|
41
107
|
}
|
|
42
108
|
|
|
43
109
|
const manifest = {
|
|
44
|
-
schemaVersion: '
|
|
110
|
+
schemaVersion: '2.0',
|
|
45
111
|
generatedAt: new Date().toISOString(),
|
|
46
112
|
repository: {
|
|
47
113
|
root: repositoryRoot,
|
|
48
|
-
gitRemote: null,
|
|
49
|
-
gitSha: null,
|
|
50
114
|
packageManager: detectPackageManager(repositoryRoot),
|
|
51
115
|
workspaceManager: detectWorkspaceManager(repositoryRoot),
|
|
52
116
|
},
|
|
53
117
|
environment: {
|
|
54
|
-
os: getManifestOs(process.platform),
|
|
55
|
-
shell: process.env.SHELL || null,
|
|
56
118
|
nodeVersion: process.version,
|
|
57
|
-
|
|
58
|
-
safeEnv: {},
|
|
119
|
+
os: getManifestOs(process.platform),
|
|
59
120
|
},
|
|
60
|
-
ciDiscovery
|
|
121
|
+
ciDiscovery,
|
|
61
122
|
frameworks: [
|
|
62
|
-
...
|
|
63
|
-
...
|
|
123
|
+
...eligible.map(detection => buildFramework(repositoryRoot, detection, ciDiscovery)),
|
|
124
|
+
...detectedOnly.map(detection => buildDetectedOnlyFramework(repositoryRoot, detection, ciDiscovery)),
|
|
125
|
+
...unsupported.map(detection => buildUnsupportedFramework(repositoryRoot, detection, ciDiscovery)),
|
|
64
126
|
],
|
|
65
127
|
omitted: [],
|
|
66
128
|
}
|
|
@@ -73,666 +135,904 @@ function createManifestScaffold ({ root, frameworks = new Set() }) {
|
|
|
73
135
|
}
|
|
74
136
|
|
|
75
137
|
/**
|
|
76
|
-
* Builds
|
|
138
|
+
* Builds one runnable direct-runner framework or a precise setup blocker.
|
|
77
139
|
*
|
|
78
140
|
* @param {string} repositoryRoot repository root
|
|
79
|
-
* @param {object} detection
|
|
80
|
-
* @
|
|
141
|
+
* @param {object} detection eligible framework detection
|
|
142
|
+
* @param {object} ciDiscovery bounded CI discovery result
|
|
143
|
+
* @returns {object} framework manifest entry
|
|
81
144
|
*/
|
|
82
|
-
function
|
|
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) || {}
|
|
145
|
+
function buildFramework (repositoryRoot, detection, ciDiscovery) {
|
|
107
146
|
const framework = detection.id
|
|
147
|
+
const packageJson = getDetectionPackageJson(repositoryRoot, detection.commandLocation)
|
|
148
|
+
const projectRoot = path.dirname(packageJson.path)
|
|
149
|
+
const base = getFrameworkBase({
|
|
150
|
+
ciDiscovery,
|
|
151
|
+
detection,
|
|
152
|
+
framework,
|
|
153
|
+
packageJson,
|
|
154
|
+
projectRoot,
|
|
155
|
+
repositoryRoot,
|
|
156
|
+
})
|
|
108
157
|
|
|
109
|
-
if (!
|
|
158
|
+
if (!SUPPORTED_FRAMEWORKS.has(framework)) {
|
|
110
159
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.`],
|
|
160
|
+
...base,
|
|
161
|
+
status: 'unsupported_by_validator',
|
|
162
|
+
notes: [`${detection.name} is supported by dd-trace, but this validator has no direct-runner adapter.`],
|
|
117
163
|
}
|
|
118
164
|
}
|
|
119
165
|
|
|
120
|
-
const
|
|
166
|
+
const projectRunner = getProjectNodeRunner(detection.command, projectRoot, repositoryRoot)
|
|
167
|
+
const runner = projectRunner || resolveRunner(framework, projectRoot, repositoryRoot)
|
|
121
168
|
if (!runner) {
|
|
122
169
|
return {
|
|
123
|
-
|
|
124
|
-
framework,
|
|
125
|
-
frameworkVersion: detection.version,
|
|
170
|
+
...base,
|
|
126
171
|
status: 'requires_manual_setup',
|
|
127
|
-
project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
|
|
128
172
|
notes: [
|
|
129
|
-
`${detection.name}
|
|
130
|
-
|
|
173
|
+
`${detection.name} is detected, but its repository-contained executable is unavailable. Install this ` +
|
|
174
|
+
'package normally, then create a fresh validation plan.',
|
|
131
175
|
],
|
|
132
176
|
}
|
|
133
177
|
}
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
178
|
+
|
|
179
|
+
const runnerContract = getRunnerContract(framework, detection.command, projectRoot, repositoryRoot)
|
|
180
|
+
if (runnerContract.error) {
|
|
181
|
+
return {
|
|
182
|
+
...base,
|
|
183
|
+
status: 'requires_manual_setup',
|
|
184
|
+
notes: [
|
|
185
|
+
`${detection.name} runner configuration was not retained because ${runnerContract.error}. ` +
|
|
186
|
+
'Choose a representative test command manually or adjust the project setup, then create a fresh plan.',
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const commandRoots = getRunnerSearchRoots(framework, detection.command, projectRoot, repositoryRoot)
|
|
191
|
+
const representativeRoot = commandRoots[0] || findPreferredRepresentativeRoot(projectRoot, repositoryRoot)
|
|
192
|
+
const representativePackage = readJson(path.join(representativeRoot, 'package.json')) || packageJson.json
|
|
193
|
+
const projectFiles = collectProjectFiles(projectRoot)
|
|
194
|
+
const candidateFiles = representativeRoot === projectRoot
|
|
195
|
+
? projectFiles
|
|
196
|
+
: collectProjectFiles(representativeRoot)
|
|
197
|
+
const candidate = selectRepresentativeTest(
|
|
198
|
+
candidateFiles,
|
|
137
199
|
framework,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
200
|
+
representativeRoot,
|
|
201
|
+
representativePackage.name,
|
|
202
|
+
commandRoots.length > 0
|
|
203
|
+
)
|
|
204
|
+
if (!candidate) {
|
|
205
|
+
return {
|
|
206
|
+
...base,
|
|
207
|
+
status: 'requires_manual_setup',
|
|
208
|
+
notes: [
|
|
209
|
+
`No single ${detection.name} test file could be selected confidently. Choose a normal framework-owned ` +
|
|
210
|
+
'test file or validate this framework manually.',
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (candidate.requiresExternalService) {
|
|
215
|
+
return {
|
|
216
|
+
...base,
|
|
217
|
+
status: 'requires_manual_setup',
|
|
218
|
+
notes: [
|
|
219
|
+
`No self-contained ${detection.name} spec could be selected. The available spec accesses a localhost ` +
|
|
220
|
+
'application that discovery will not start. Start the application through the project\'s normal setup and ' +
|
|
221
|
+
'validate Cypress manually, or add a self-contained spec before creating a fresh plan.',
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
}
|
|
148
225
|
|
|
149
226
|
const generatedTestStrategy = buildGeneratedTestStrategy({
|
|
150
|
-
baseCommand: preserveProjectWrapper ? baseCommand : undefined,
|
|
151
227
|
framework,
|
|
152
|
-
packageJson,
|
|
153
228
|
projectRoot,
|
|
154
|
-
representative,
|
|
155
|
-
runner,
|
|
229
|
+
representative: candidate.path,
|
|
156
230
|
})
|
|
231
|
+
const configFiles = [...new Set([
|
|
232
|
+
...runnerContract.inputFiles,
|
|
233
|
+
...getImplicitConfigFiles(framework, projectRoot, repositoryRoot),
|
|
234
|
+
...(representativeRoot === projectRoot
|
|
235
|
+
? []
|
|
236
|
+
: getImplicitConfigFiles(framework, representativeRoot, repositoryRoot)),
|
|
237
|
+
...projectFiles.filter(filename => CONFIG_PATTERNS[framework]?.test(path.basename(filename))).slice(0, 5),
|
|
238
|
+
])]
|
|
239
|
+
if (configFiles.length > 20) {
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
status: 'requires_manual_setup',
|
|
243
|
+
notes: [
|
|
244
|
+
`${detection.name} loads more configuration files than the validator can approval-bind safely. ` +
|
|
245
|
+
'Use a narrower project setup before creating a fresh plan.',
|
|
246
|
+
],
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const runnerDescription = projectRunner ? 'repository test wrapper' : `installed ${framework} runner`
|
|
250
|
+
const browserRequired = framework === 'cypress' ||
|
|
251
|
+
framework === 'playwright' ||
|
|
252
|
+
(framework === 'vitest' && runnerContract.runnerArgs.includes('--browser'))
|
|
157
253
|
|
|
158
254
|
return {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
255
|
+
...base,
|
|
256
|
+
browserRequired,
|
|
257
|
+
language: /\.[cm]?tsx?$/.test(candidate.path) ? 'typescript' : 'javascript',
|
|
258
|
+
localSocketRequired: candidate.requiresLocalSocket,
|
|
163
259
|
status: 'runnable',
|
|
164
|
-
supportLevel: '
|
|
165
|
-
project:
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
},
|
|
260
|
+
supportLevel: 'validator_direct_runner',
|
|
261
|
+
project: {
|
|
262
|
+
...base.project,
|
|
263
|
+
configFiles,
|
|
180
264
|
},
|
|
265
|
+
validation: {
|
|
266
|
+
environment: runnerContract.environment,
|
|
267
|
+
...(runnerContract.omittedOptions?.length > 0
|
|
268
|
+
? { omittedRunnerOptions: runnerContract.omittedOptions }
|
|
269
|
+
: {}),
|
|
270
|
+
requiredEnvVars: [],
|
|
271
|
+
runner,
|
|
272
|
+
runnerArgs: runnerContract.runnerArgs,
|
|
273
|
+
selectorScope: projectRunner ? 'instrumented_event_identity' : 'bounded_direct_runner',
|
|
274
|
+
testFile: candidate.path,
|
|
275
|
+
timeoutMs: browserRequired ? 300_000 : 180_000,
|
|
276
|
+
},
|
|
277
|
+
preflight: { status: 'pending' },
|
|
181
278
|
generatedTestStrategy,
|
|
182
279
|
notes: [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
280
|
+
`Basic Reporting will invoke the ${runnerDescription} ` +
|
|
281
|
+
'directly for ' +
|
|
282
|
+
`${path.relative(repositoryRoot, candidate.path)}.`,
|
|
283
|
+
'The validator retained only allowlisted runner configuration from the detected package script.',
|
|
284
|
+
...(projectRunner
|
|
285
|
+
? [
|
|
286
|
+
'Basic Reporting will remain incomplete unless captured test events identify only the approved ' +
|
|
287
|
+
'representative file.',
|
|
288
|
+
]
|
|
289
|
+
: []),
|
|
290
|
+
...(candidate.requiresLocalSocket
|
|
291
|
+
? [
|
|
292
|
+
'The selected test appears to require localhost. A restricted sandbox may leave local validation ' +
|
|
293
|
+
'incomplete.',
|
|
294
|
+
]
|
|
295
|
+
: []),
|
|
190
296
|
],
|
|
191
297
|
}
|
|
192
298
|
}
|
|
193
299
|
|
|
194
|
-
function
|
|
300
|
+
function getImplicitConfigFiles (framework, projectRoot, repositoryRoot) {
|
|
301
|
+
const files = []
|
|
302
|
+
for (const basename of IMPLICIT_CONFIG_FILENAMES[framework] || []) {
|
|
303
|
+
const filename = path.join(projectRoot, basename)
|
|
304
|
+
try {
|
|
305
|
+
const stat = fs.lstatSync(filename)
|
|
306
|
+
const physical = fs.realpathSync(filename)
|
|
307
|
+
if (stat.isFile() && !stat.isSymbolicLink() &&
|
|
308
|
+
fs.statSync(physical).isFile() &&
|
|
309
|
+
isPathInside(fs.realpathSync(repositoryRoot), physical)) files.push(physical)
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
return files
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Builds a diagnostic-only supported framework entry.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} repositoryRoot repository root
|
|
319
|
+
* @param {object} detection supported framework detection
|
|
320
|
+
* @param {object} ciDiscovery bounded CI discovery result
|
|
321
|
+
* @returns {object} framework manifest entry
|
|
322
|
+
*/
|
|
323
|
+
function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery) {
|
|
324
|
+
const packageJson = getDetectionPackageJson(repositoryRoot, detection.locations?.[0])
|
|
325
|
+
const projectRoot = path.dirname(packageJson.path)
|
|
326
|
+
const version = detection.supportedVersion?.version ||
|
|
327
|
+
detection.versionDetections?.[0]?.version ||
|
|
328
|
+
detection.versionDetections?.[0]?.rawVersion
|
|
195
329
|
return {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
330
|
+
...getFrameworkBase({
|
|
331
|
+
ciDiscovery,
|
|
332
|
+
detection: { ...detection, version },
|
|
333
|
+
framework: detection.id,
|
|
334
|
+
packageJson,
|
|
335
|
+
projectRoot,
|
|
336
|
+
repositoryRoot,
|
|
337
|
+
}),
|
|
338
|
+
status: 'detected_not_runnable',
|
|
339
|
+
notes: [
|
|
340
|
+
detection.supportedVersion
|
|
341
|
+
? `A supported ${detection.name} installation was detected, but no project-local validation target was found.`
|
|
342
|
+
: `${detection.name} was detected, but no supported installed version was confirmed.`,
|
|
343
|
+
],
|
|
201
344
|
}
|
|
202
345
|
}
|
|
203
346
|
|
|
204
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Builds an unsupported framework entry.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} repositoryRoot repository root
|
|
351
|
+
* @param {object} detection unsupported framework detection
|
|
352
|
+
* @param {object} ciDiscovery bounded CI discovery result
|
|
353
|
+
* @returns {object} framework manifest entry
|
|
354
|
+
*/
|
|
355
|
+
function buildUnsupportedFramework (repositoryRoot, detection, ciDiscovery) {
|
|
356
|
+
const packageJson = getDetectionPackageJson(repositoryRoot, detection.locations?.[0])
|
|
357
|
+
const projectRoot = path.dirname(packageJson.path)
|
|
358
|
+
const framework = detection.id === 'node-test' ? 'node:test' : detection.id
|
|
359
|
+
return {
|
|
360
|
+
...getFrameworkBase({
|
|
361
|
+
ciDiscovery,
|
|
362
|
+
detection,
|
|
363
|
+
framework,
|
|
364
|
+
packageJson,
|
|
365
|
+
projectRoot,
|
|
366
|
+
repositoryRoot,
|
|
367
|
+
}),
|
|
368
|
+
status: 'unsupported_by_validator',
|
|
369
|
+
notes: [`${detection.name} is not supported by this Test Optimization validator.`],
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Returns fields shared by runnable and diagnostic framework entries.
|
|
375
|
+
*
|
|
376
|
+
* @param {object} input framework inputs
|
|
377
|
+
* @param {object} input.ciDiscovery bounded CI discovery
|
|
378
|
+
* @param {object} input.detection framework detection
|
|
379
|
+
* @param {string} input.framework framework name
|
|
380
|
+
* @param {object} input.packageJson package metadata
|
|
381
|
+
* @param {string} input.projectRoot project root
|
|
382
|
+
* @param {string} input.repositoryRoot repository root
|
|
383
|
+
* @returns {object} shared framework fields
|
|
384
|
+
*/
|
|
385
|
+
function getFrameworkBase ({
|
|
386
|
+
ciDiscovery,
|
|
387
|
+
detection,
|
|
205
388
|
framework,
|
|
389
|
+
packageJson,
|
|
206
390
|
projectRoot,
|
|
207
391
|
repositoryRoot,
|
|
208
|
-
runner,
|
|
209
|
-
scriptName,
|
|
210
|
-
preserveProjectWrapper,
|
|
211
392
|
}) {
|
|
212
|
-
const packageManager = preserveProjectWrapper ? detectPackageManager(repositoryRoot) : undefined
|
|
213
|
-
const argv = preserveProjectWrapper
|
|
214
|
-
? getPackageScriptArgv(packageManager, scriptName, repositoryRoot)
|
|
215
|
-
: getDirectRunnerArgv(framework, runner)
|
|
216
393
|
return {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
394
|
+
id: `${framework}:${getProjectId(packageJson.json, projectRoot, repositoryRoot)}`,
|
|
395
|
+
framework,
|
|
396
|
+
frameworkVersion: detection.version || detection.supportedVersion?.version || null,
|
|
397
|
+
language: 'unknown',
|
|
398
|
+
supportLevel: 'detected_only',
|
|
399
|
+
project: {
|
|
400
|
+
configFiles: [],
|
|
401
|
+
name: packageJson.json.name || path.basename(projectRoot) || 'root',
|
|
402
|
+
packageJson: packageJson.path,
|
|
403
|
+
root: projectRoot,
|
|
404
|
+
},
|
|
405
|
+
ciWiring: buildCiWiring(ciDiscovery),
|
|
226
406
|
}
|
|
227
407
|
}
|
|
228
408
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Builds inert CI review fields. The agent may populate only this evidence section.
|
|
411
|
+
*
|
|
412
|
+
* @param {object} ciDiscovery bounded CI discovery result
|
|
413
|
+
* @returns {object} CI evidence scaffold
|
|
414
|
+
*/
|
|
415
|
+
function buildCiWiring (ciDiscovery) {
|
|
416
|
+
const unresolved = ciDiscovery.reviewRequired
|
|
417
|
+
? ['The exact CI test job and its effective environment have not been reviewed.']
|
|
418
|
+
: ['No supported CI configuration file was found by bounded discovery.']
|
|
235
419
|
return {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
],
|
|
420
|
+
command: null,
|
|
421
|
+
configFile: null,
|
|
422
|
+
initialization: { evidence: [], status: 'unknown' },
|
|
423
|
+
job: null,
|
|
424
|
+
reviewComplete: false,
|
|
425
|
+
step: null,
|
|
426
|
+
transport: { evidence: [], mode: 'unknown' },
|
|
427
|
+
unresolved,
|
|
428
|
+
workingDirectory: null,
|
|
271
429
|
}
|
|
272
430
|
}
|
|
273
431
|
|
|
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
432
|
/**
|
|
281
|
-
*
|
|
433
|
+
* Builds canonical generated test data without embedding executable commands.
|
|
282
434
|
*
|
|
283
|
-
* @param {
|
|
284
|
-
* @param {string}
|
|
285
|
-
* @param {string
|
|
286
|
-
* @
|
|
435
|
+
* @param {object} input generated-test inputs
|
|
436
|
+
* @param {string} input.framework framework name
|
|
437
|
+
* @param {string} input.projectRoot project root
|
|
438
|
+
* @param {string} input.representative representative test file
|
|
439
|
+
* @returns {object} generated test strategy
|
|
287
440
|
*/
|
|
288
|
-
function
|
|
289
|
-
const
|
|
290
|
-
|
|
441
|
+
function buildGeneratedTestStrategy ({ framework, projectRoot, representative }) {
|
|
442
|
+
const convention = getGeneratedTestConvention(framework, representative, projectRoot)
|
|
443
|
+
const testDirectory = convention.testDirectory
|
|
444
|
+
const extension = convention.fileExtension
|
|
445
|
+
const moduleSystem = getModuleSystem(representative, projectRoot)
|
|
446
|
+
const files = []
|
|
447
|
+
const scenarios = []
|
|
448
|
+
const cleanupPaths = []
|
|
291
449
|
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
450
|
+
for (const [id, scenario] of Object.entries(GENERATED_SCENARIOS)) {
|
|
451
|
+
const prefix = `dd-test-optimization-validation-${framework}-${id}`
|
|
452
|
+
const filename = convention.exactFilename
|
|
453
|
+
? path.join(testDirectory, prefix, convention.exactFilename)
|
|
454
|
+
: path.join(testDirectory, `${prefix}${extension}`)
|
|
455
|
+
const content = getGeneratedTestContent({
|
|
456
|
+
framework,
|
|
457
|
+
moduleSystem,
|
|
458
|
+
scenarioId: id,
|
|
459
|
+
stateFile: ['cucumber', 'cypress', 'playwright'].includes(framework)
|
|
460
|
+
? undefined
|
|
461
|
+
: getGeneratedRetryStatePath(framework, filename),
|
|
462
|
+
})
|
|
463
|
+
files.push({ path: filename, contentLines: content.split('\n') })
|
|
464
|
+
cleanupPaths.push(filename)
|
|
465
|
+
scenarios.push({
|
|
466
|
+
id,
|
|
467
|
+
expectedWithoutDatadog: scenario.expectedWithoutDatadog,
|
|
468
|
+
testIdentities: [{
|
|
469
|
+
file: filename,
|
|
470
|
+
name: scenario.testName,
|
|
471
|
+
suite: scenario.suiteName,
|
|
472
|
+
}],
|
|
473
|
+
})
|
|
297
474
|
}
|
|
298
475
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
testDirectory: path.join(projectRoot, 'test'),
|
|
308
|
-
}
|
|
476
|
+
if (framework === 'cucumber') {
|
|
477
|
+
const stepsFile = cucumber.getGeneratedStepsPath(testDirectory)
|
|
478
|
+
files.push({ path: stepsFile, contentLines: cucumber.getGeneratedStepsContent().split('\n') })
|
|
479
|
+
cleanupPaths.push(stepsFile)
|
|
480
|
+
} else if (framework === 'playwright') {
|
|
481
|
+
const configFile = playwright.getGeneratedConfigPath(testDirectory)
|
|
482
|
+
files.push({ path: configFile, contentLines: playwright.getGeneratedConfigContent().split('\n') })
|
|
483
|
+
cleanupPaths.push(configFile)
|
|
309
484
|
}
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
}
|
|
485
|
+
if (['jest', 'mocha', 'vitest'].includes(framework)) {
|
|
486
|
+
cleanupPaths.push(getGeneratedRetryStatePath(framework, scenarios[1].testIdentities[0].file))
|
|
321
487
|
}
|
|
322
488
|
|
|
323
489
|
return {
|
|
324
|
-
|
|
325
|
-
fileExtension:
|
|
326
|
-
|
|
490
|
+
cleanupPaths,
|
|
491
|
+
fileExtension: extension,
|
|
492
|
+
files,
|
|
493
|
+
moduleSystem,
|
|
494
|
+
reason: 'Validator-owned direct-runner recipes.',
|
|
495
|
+
scenarios,
|
|
496
|
+
status: 'planned',
|
|
497
|
+
testDirectory,
|
|
327
498
|
}
|
|
328
499
|
}
|
|
329
500
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Selects one representative framework-owned test file.
|
|
503
|
+
*
|
|
504
|
+
* @param {string[]} files bounded project files
|
|
505
|
+
* @param {string} framework framework name
|
|
506
|
+
* @param {string} projectRoot project root
|
|
507
|
+
* @param {string} packageName project package name
|
|
508
|
+
* @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
|
|
509
|
+
* @returns {{path: string, requiresExternalService: boolean, requiresLocalSocket: boolean}|undefined} selected test
|
|
510
|
+
*/
|
|
511
|
+
function selectRepresentativeTest (files, framework, projectRoot, packageName, allowDirectoryConvention) {
|
|
512
|
+
const candidates = []
|
|
513
|
+
for (const filename of files) {
|
|
514
|
+
const source = readText(filename)
|
|
515
|
+
if (source === undefined ||
|
|
516
|
+
!isTestFile(filename, source, framework, projectRoot, allowDirectoryConvention) ||
|
|
517
|
+
hasConflictingFramework(source, framework)) continue
|
|
518
|
+
if (framework === 'cucumber'
|
|
519
|
+
? cucumber.getScenarioCount(source) === 0
|
|
520
|
+
: getStaticTestCount(source) === 0) continue
|
|
521
|
+
if (!hasFrameworkOwnership(filename, source, framework, packageName)) continue
|
|
522
|
+
candidates.push({
|
|
523
|
+
path: filename,
|
|
524
|
+
rank: getTestRank(filename, source, projectRoot),
|
|
525
|
+
requiresExternalService: framework === 'cypress' && CYPRESS_LOCAL_ORIGIN_PATTERN.test(source),
|
|
526
|
+
requiresLocalSocket: LOCAL_SOCKET_PATTERN.test(source),
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
candidates.sort((left, right) => {
|
|
530
|
+
return Number(left.requiresExternalService) - Number(right.requiresExternalService) ||
|
|
531
|
+
left.rank - right.rank ||
|
|
532
|
+
left.path.localeCompare(right.path)
|
|
533
|
+
})
|
|
534
|
+
return candidates[0]
|
|
535
|
+
}
|
|
339
536
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Reports whether a file follows a selected framework's test convention.
|
|
539
|
+
*
|
|
540
|
+
* @param {string} filename candidate file
|
|
541
|
+
* @param {string} source candidate source
|
|
542
|
+
* @param {string} framework framework name
|
|
543
|
+
* @param {string} projectRoot project root
|
|
544
|
+
* @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
|
|
545
|
+
* @returns {boolean} whether the file is a candidate
|
|
546
|
+
*/
|
|
547
|
+
function isTestFile (filename, source, framework, projectRoot, allowDirectoryConvention) {
|
|
548
|
+
const basename = path.basename(filename)
|
|
549
|
+
const normalized = filename.replaceAll('\\', '/')
|
|
550
|
+
if (normalized.includes('/dd-test-optimization-validation-')) return false
|
|
551
|
+
if (TYPE_ONLY_TEST_PATTERN.test(basename) || TYPE_ONLY_DIRECTORY_PATTERN.test(normalized)) return false
|
|
552
|
+
if (framework === 'cucumber') return cucumber.isTestFile(filename)
|
|
553
|
+
if (framework === 'cypress') return cypress.isTestFile(basename, path.dirname(filename), projectRoot)
|
|
554
|
+
if (framework === 'playwright') return playwright.isTestFile(basename, path.dirname(filename), projectRoot)
|
|
555
|
+
if (TEST_FILE_PATTERN.test(basename)) return true
|
|
556
|
+
const directories = [
|
|
557
|
+
path.basename(projectRoot),
|
|
558
|
+
...path.relative(projectRoot, path.dirname(filename)).split(path.sep),
|
|
559
|
+
]
|
|
560
|
+
const inTestDirectory = directories.some(directory => {
|
|
561
|
+
return ['__tests__', 'spec', 'test', 'tests'].includes(directory)
|
|
350
562
|
})
|
|
563
|
+
if (BARE_TEST_FILE_PATTERN.test(basename)) {
|
|
564
|
+
return inTestDirectory || hasExplicitFrameworkImport(source, framework)
|
|
565
|
+
}
|
|
566
|
+
return allowDirectoryConvention && inTestDirectory && /\.[cm]?[jt]sx?$/.test(basename)
|
|
351
567
|
}
|
|
352
568
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
569
|
+
/**
|
|
570
|
+
* Reports whether an unconventional test file imports the selected framework directly.
|
|
571
|
+
*
|
|
572
|
+
* @param {string} source candidate source
|
|
573
|
+
* @param {string} framework framework name
|
|
574
|
+
* @returns {boolean} whether ownership is explicit
|
|
575
|
+
*/
|
|
576
|
+
function hasExplicitFrameworkImport (source, framework) {
|
|
577
|
+
if (framework === 'vitest') {
|
|
578
|
+
return /(?:from\s+|require\s*\(\s*)['"]vitest['"]/.test(source)
|
|
357
579
|
}
|
|
358
|
-
if (framework === '
|
|
359
|
-
|
|
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
|
-
}
|
|
580
|
+
if (framework === 'jest') {
|
|
581
|
+
return /(?:from\s+|require\s*\(\s*)['"]@jest\/globals['"]/.test(source)
|
|
373
582
|
}
|
|
374
|
-
|
|
375
|
-
|
|
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,
|
|
583
|
+
if (framework === 'mocha') {
|
|
584
|
+
return /(?:from\s+|require\s*\(\s*)['"]mocha['"]/.test(source)
|
|
417
585
|
}
|
|
586
|
+
return false
|
|
418
587
|
}
|
|
419
588
|
|
|
420
589
|
/**
|
|
421
|
-
*
|
|
590
|
+
* Conservatively associates a test source with one framework.
|
|
422
591
|
*
|
|
423
|
-
* @param {
|
|
424
|
-
* @param {string}
|
|
425
|
-
* @param {string}
|
|
426
|
-
* @param {
|
|
427
|
-
* @
|
|
428
|
-
* @param {string} [moduleSystem] generated test module system
|
|
429
|
-
* @returns {object} focused project command
|
|
592
|
+
* @param {string} filename candidate filename
|
|
593
|
+
* @param {string} source file source
|
|
594
|
+
* @param {string} framework framework name
|
|
595
|
+
* @param {string} packageName project package name
|
|
596
|
+
* @returns {boolean} whether ownership is plausible
|
|
430
597
|
*/
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
framework
|
|
434
|
-
filename
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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,
|
|
598
|
+
function hasFrameworkOwnership (filename, source, framework, packageName) {
|
|
599
|
+
const normalized = filename.replaceAll('\\', '/').toLowerCase()
|
|
600
|
+
if (framework === 'cucumber') return /^\s*(?:Feature|Rule):/m.test(source)
|
|
601
|
+
if (framework === 'cypress') return /\.cy\./.test(filename) || normalized.includes('/cypress/')
|
|
602
|
+
if (framework === 'playwright') {
|
|
603
|
+
return /@playwright\/test/.test(source) || normalized.includes('/playwright/') ||
|
|
604
|
+
normalized.includes('/e2e/')
|
|
447
605
|
}
|
|
606
|
+
if (framework === 'vitest') return /\bvitest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
|
|
607
|
+
if (framework === 'jest') return /\bjest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
|
|
608
|
+
if (framework === 'mocha') {
|
|
609
|
+
return /\bmocha\b/.test(source) || /\bdescribe\s*\(/.test(source) ||
|
|
610
|
+
packageName === 'mocha'
|
|
611
|
+
}
|
|
612
|
+
return false
|
|
448
613
|
}
|
|
449
614
|
|
|
450
615
|
/**
|
|
451
|
-
*
|
|
616
|
+
* Rejects files visibly owned by another supported runner.
|
|
452
617
|
*
|
|
453
|
-
* @param {string}
|
|
454
|
-
* @param {string}
|
|
455
|
-
* @
|
|
456
|
-
* @param {string} [moduleSystem] generated test module system
|
|
457
|
-
* @returns {string[]} focused test arguments
|
|
618
|
+
* @param {string} source candidate source
|
|
619
|
+
* @param {string} framework selected framework
|
|
620
|
+
* @returns {boolean} whether source ownership conflicts
|
|
458
621
|
*/
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
'--no-watchman',
|
|
467
|
-
]
|
|
622
|
+
function hasConflictingFramework (source, framework) {
|
|
623
|
+
const markers = {
|
|
624
|
+
cypress: /\bcy\.(?:visit|request|get)\b|from\s+['"]cypress/,
|
|
625
|
+
jest: /@jest\/globals|\bjest\.(?:mock|fn|spyOn)\b/,
|
|
626
|
+
'node:test': /(?:from\s+|require\s*\(\s*)['"]node:test['"]/,
|
|
627
|
+
playwright: /@playwright\/test/,
|
|
628
|
+
vitest: /from\s+['"]vitest['"]|require\s*\(\s*['"]vitest['"]\s*\)|\bvi\./,
|
|
468
629
|
}
|
|
469
|
-
return [
|
|
630
|
+
return Object.entries(markers).some(([name, pattern]) => name !== framework && pattern.test(source))
|
|
470
631
|
}
|
|
471
632
|
|
|
472
633
|
/**
|
|
473
|
-
*
|
|
474
|
-
* declares the dependency.
|
|
634
|
+
* Ranks simpler unit tests ahead of integration and service-dependent tests.
|
|
475
635
|
*
|
|
476
|
-
* @param {string}
|
|
477
|
-
* @param {string}
|
|
478
|
-
* @
|
|
636
|
+
* @param {string} filename candidate filename
|
|
637
|
+
* @param {string} source candidate source
|
|
638
|
+
* @param {string} projectRoot project root
|
|
639
|
+
* @returns {number} lower-is-better rank
|
|
479
640
|
*/
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
641
|
+
function getTestRank (filename, source, projectRoot) {
|
|
642
|
+
const relative = path.relative(projectRoot, filename).replaceAll('\\', '/').toLowerCase()
|
|
643
|
+
let rank = relative.split('/').length
|
|
644
|
+
if (/(?:^|\/)(?:test|tests|__tests__|spec)\//.test(relative)) rank -= 20
|
|
645
|
+
if (/(?:^|[._/-])unit(?:[._/-]|$)/.test(relative)) rank -= 15
|
|
646
|
+
if (/(?:e2e|integration|acceptance|browser|conformance|fixtures?)/.test(relative)) rank += 20
|
|
647
|
+
if (LOCAL_SOCKET_PATTERN.test(source)) rank += 30
|
|
648
|
+
rank += Math.min(getStaticTestCount(source), 50)
|
|
649
|
+
return rank
|
|
484
650
|
}
|
|
485
651
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
652
|
+
/**
|
|
653
|
+
* Counts static test declarations for preferring small representative files.
|
|
654
|
+
*
|
|
655
|
+
* @param {string} source test source
|
|
656
|
+
* @returns {number} approximate test declaration count
|
|
657
|
+
*/
|
|
658
|
+
function getStaticTestCount (source) {
|
|
659
|
+
return [...source.matchAll(/\b(?:it|test)((?:\.[A-Za-z]+)*)\s*\(/g)]
|
|
660
|
+
.filter(match => !/\.(?:skip|todo)\b/.test(match[1]))
|
|
661
|
+
.length
|
|
492
662
|
}
|
|
493
663
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Narrows a repository root to the conventional package matching its identity.
|
|
666
|
+
*
|
|
667
|
+
* @param {string} projectRoot detected project root
|
|
668
|
+
* @param {string} repositoryRoot repository root
|
|
669
|
+
* @returns {string} preferred representative root
|
|
670
|
+
*/
|
|
671
|
+
function findPreferredRepresentativeRoot (projectRoot, repositoryRoot) {
|
|
672
|
+
if (path.resolve(projectRoot) !== path.resolve(repositoryRoot)) return projectRoot
|
|
673
|
+
const repositoryName = normalizeProjectIdentity(path.basename(repositoryRoot))
|
|
674
|
+
|
|
675
|
+
for (const containerName of ['packages', 'pkgs', 'modules']) {
|
|
676
|
+
const container = path.join(repositoryRoot, containerName)
|
|
677
|
+
for (const entry of readDirectoryEntries(container)) {
|
|
678
|
+
if (!entry.isDirectory()) continue
|
|
679
|
+
const candidate = path.join(container, entry.name)
|
|
680
|
+
const packageJson = readJson(path.join(candidate, 'package.json'))
|
|
681
|
+
if (normalizeProjectIdentity(packageJson?.name || entry.name) === repositoryName) return candidate
|
|
682
|
+
}
|
|
498
683
|
}
|
|
499
|
-
return
|
|
684
|
+
return projectRoot
|
|
500
685
|
}
|
|
501
686
|
|
|
502
687
|
/**
|
|
503
|
-
*
|
|
688
|
+
* Normalizes repository and package names for exact identity comparison.
|
|
504
689
|
*
|
|
505
|
-
* @param {
|
|
506
|
-
* @
|
|
507
|
-
* @returns {string|undefined} package script name
|
|
690
|
+
* @param {string} value package identity
|
|
691
|
+
* @returns {string} normalized identity
|
|
508
692
|
*/
|
|
509
|
-
function
|
|
510
|
-
|
|
693
|
+
function normalizeProjectIdentity (value) {
|
|
694
|
+
const unscoped = String(value || '').toLowerCase().replace(/^@[^/]+\//, '')
|
|
695
|
+
return unscoped.replaceAll(/[^a-z0-9]+/g, '').replace(/js$/, '')
|
|
511
696
|
}
|
|
512
697
|
|
|
513
698
|
/**
|
|
514
|
-
*
|
|
699
|
+
* Collects bounded regular project files without following symbolic links.
|
|
515
700
|
*
|
|
516
|
-
* @param {string}
|
|
517
|
-
* @
|
|
518
|
-
* @returns {boolean} whether the command starts with the framework binary
|
|
701
|
+
* @param {string} root project root
|
|
702
|
+
* @returns {string[]} absolute files
|
|
519
703
|
*/
|
|
520
|
-
function
|
|
521
|
-
const
|
|
522
|
-
const
|
|
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()
|
|
704
|
+
function collectProjectFiles (root) {
|
|
705
|
+
const files = []
|
|
706
|
+
const pending = [root]
|
|
535
707
|
let visited = 0
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
708
|
+
|
|
709
|
+
while (pending.length > 0 && visited < MAX_DISCOVERY_ENTRIES) {
|
|
710
|
+
const directory = pending.shift()
|
|
711
|
+
for (const entry of readDirectoryEntries(directory)) {
|
|
712
|
+
if (++visited > MAX_DISCOVERY_ENTRIES) break
|
|
547
713
|
const filename = path.join(directory, entry.name)
|
|
714
|
+
if (entry.isSymbolicLink()) continue
|
|
548
715
|
if (entry.isDirectory()) {
|
|
549
|
-
|
|
550
|
-
|
|
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)
|
|
716
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) pending.push(filename)
|
|
717
|
+
} else if (entry.isFile()) {
|
|
718
|
+
files.push(filename)
|
|
557
719
|
}
|
|
558
720
|
}
|
|
559
721
|
}
|
|
722
|
+
return files
|
|
723
|
+
}
|
|
560
724
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Resolves a framework package executable only inside the repository.
|
|
727
|
+
*
|
|
728
|
+
* @param {string} framework framework name
|
|
729
|
+
* @param {string} projectRoot project root
|
|
730
|
+
* @param {string} repositoryRoot repository root
|
|
731
|
+
* @returns {string|undefined} physical runner path
|
|
732
|
+
*/
|
|
733
|
+
function resolveRunner (framework, projectRoot, repositoryRoot) {
|
|
734
|
+
const packageName = getRunnerPackageName(framework)
|
|
735
|
+
const packageRoot = findPackageRoot(packageName, projectRoot, repositoryRoot)
|
|
736
|
+
if (!packageRoot) return
|
|
737
|
+
|
|
738
|
+
const packageJson = readJson(path.join(packageRoot, 'package.json'))
|
|
739
|
+
const binName = framework === 'cucumber' ? 'cucumber-js' : framework === 'playwright' ? 'playwright' : framework
|
|
740
|
+
const bin = typeof packageJson?.bin === 'string' ? packageJson.bin : packageJson?.bin?.[binName]
|
|
741
|
+
if (typeof bin !== 'string') return
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
const runner = fs.realpathSync(path.resolve(packageRoot, bin))
|
|
745
|
+
return isPathInside(fs.realpathSync(repositoryRoot), runner) && fs.statSync(runner).isFile()
|
|
746
|
+
? runner
|
|
747
|
+
: undefined
|
|
748
|
+
} catch {}
|
|
569
749
|
}
|
|
570
750
|
|
|
571
751
|
/**
|
|
572
|
-
*
|
|
752
|
+
* Finds an installed package or a framework source checkout.
|
|
573
753
|
*
|
|
574
|
-
* @param {string}
|
|
575
|
-
* @param {string}
|
|
576
|
-
* @
|
|
754
|
+
* @param {string} packageName runner package name
|
|
755
|
+
* @param {string} projectRoot project root
|
|
756
|
+
* @param {string} repositoryRoot repository root
|
|
757
|
+
* @returns {string|undefined} physical package root
|
|
577
758
|
*/
|
|
578
|
-
function
|
|
579
|
-
const
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
759
|
+
function findPackageRoot (packageName, projectRoot, repositoryRoot) {
|
|
760
|
+
const ownPackage = readJson(path.join(projectRoot, 'package.json'))
|
|
761
|
+
if (ownPackage?.name === packageName) return projectRoot
|
|
762
|
+
|
|
763
|
+
let directory = projectRoot
|
|
764
|
+
while (isPathInside(repositoryRoot, directory)) {
|
|
765
|
+
const candidate = path.join(directory, 'node_modules', ...packageName.split('/'))
|
|
766
|
+
try {
|
|
767
|
+
const physical = fs.realpathSync(candidate)
|
|
768
|
+
if (isPathInside(fs.realpathSync(repositoryRoot), physical)) return physical
|
|
769
|
+
} catch {}
|
|
770
|
+
if (directory === repositoryRoot) break
|
|
771
|
+
directory = path.dirname(directory)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Returns a runner package name.
|
|
777
|
+
*
|
|
778
|
+
* @param {string} framework framework name
|
|
779
|
+
* @returns {string} package name
|
|
780
|
+
*/
|
|
781
|
+
function getRunnerPackageName (framework) {
|
|
782
|
+
return {
|
|
783
|
+
cucumber: '@cucumber/cucumber',
|
|
784
|
+
playwright: '@playwright/test',
|
|
785
|
+
}[framework] || framework
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Returns a generated-test location that follows the representative discovery convention.
|
|
790
|
+
*
|
|
791
|
+
* @param {string} framework framework name
|
|
792
|
+
* @param {string} representative representative test
|
|
793
|
+
* @param {string} projectRoot detected project root
|
|
794
|
+
* @returns {{exactFilename: string|undefined, fileExtension: string, testDirectory: string}} convention
|
|
795
|
+
*/
|
|
796
|
+
function getGeneratedTestConvention (framework, representative, projectRoot) {
|
|
797
|
+
const basename = path.basename(representative)
|
|
798
|
+
if (/^test\.[cm]?[jt]s$/.test(basename)) {
|
|
799
|
+
const representativeDirectory = path.dirname(representative)
|
|
800
|
+
return {
|
|
801
|
+
exactFilename: basename,
|
|
802
|
+
fileExtension: path.extname(basename),
|
|
803
|
+
testDirectory: representativeDirectory === projectRoot
|
|
804
|
+
? projectRoot
|
|
805
|
+
: path.dirname(representativeDirectory),
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
exactFilename: undefined,
|
|
810
|
+
fileExtension: getTestExtension(framework, representative),
|
|
811
|
+
testDirectory: path.dirname(representative),
|
|
812
|
+
}
|
|
583
813
|
}
|
|
584
814
|
|
|
585
815
|
/**
|
|
586
|
-
*
|
|
816
|
+
* Returns the complete suffix needed for framework discovery.
|
|
587
817
|
*
|
|
588
|
-
* @param {string}
|
|
589
|
-
* @param {string}
|
|
590
|
-
* @returns {
|
|
818
|
+
* @param {string} framework framework name
|
|
819
|
+
* @param {string} representative representative test
|
|
820
|
+
* @returns {string} generated test suffix
|
|
591
821
|
*/
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
822
|
+
function getTestExtension (framework, representative) {
|
|
823
|
+
if (framework === 'cucumber') return '.feature'
|
|
824
|
+
if (framework === 'cypress') return cypress.getTestExtension(representative)
|
|
825
|
+
if (framework === 'playwright') return playwright.getTestExtension(representative)
|
|
826
|
+
const match = /((?:[.-](?:test|spec))\.[cm]?[jt]sx?)$/.exec(representative)
|
|
827
|
+
if (match) return match[1]
|
|
828
|
+
const moduleExtension = /\.((?:[cm]js|[cm]ts))$/.exec(representative)?.[1]
|
|
829
|
+
return moduleExtension ? `.test.${moduleExtension}` : '.test.js'
|
|
595
830
|
}
|
|
596
831
|
|
|
597
832
|
/**
|
|
598
|
-
*
|
|
833
|
+
* Determines the generated JavaScript module system.
|
|
599
834
|
*
|
|
600
|
-
* @param {string}
|
|
601
|
-
* @param {string}
|
|
602
|
-
* @
|
|
603
|
-
* @returns {number} independent test project rank
|
|
835
|
+
* @param {string} representative representative test
|
|
836
|
+
* @param {string} projectRoot project root
|
|
837
|
+
* @returns {'commonjs'|'esm'} module system
|
|
604
838
|
*/
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (cache.has(directory)) return cache.get(directory)
|
|
839
|
+
function getModuleSystem (representative, projectRoot) {
|
|
840
|
+
if (/\.(?:mjs|mts)$/.test(representative)) return 'esm'
|
|
841
|
+
if (/\.(?:cjs|cts)$/.test(representative)) return 'commonjs'
|
|
609
842
|
|
|
843
|
+
let directory = path.dirname(representative)
|
|
844
|
+
while (isPathInside(projectRoot, directory)) {
|
|
610
845
|
const packageJson = readJson(path.join(directory, 'package.json'))
|
|
611
|
-
if (packageJson)
|
|
612
|
-
|
|
613
|
-
cache.set(directory, rank)
|
|
614
|
-
return rank
|
|
615
|
-
}
|
|
846
|
+
if (packageJson) return packageJson.type === 'module' ? 'esm' : 'commonjs'
|
|
847
|
+
if (directory === projectRoot) break
|
|
616
848
|
directory = path.dirname(directory)
|
|
617
849
|
}
|
|
618
|
-
return
|
|
850
|
+
return 'commonjs'
|
|
619
851
|
}
|
|
620
852
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
853
|
+
/**
|
|
854
|
+
* Discovers bounded CI configuration paths without interpreting them.
|
|
855
|
+
*
|
|
856
|
+
* @param {string} root repository root
|
|
857
|
+
* @returns {object} CI discovery data
|
|
858
|
+
*/
|
|
859
|
+
function discoverCiFiles (root) {
|
|
860
|
+
const found = []
|
|
861
|
+
for (const relativePath of CI_PATHS) {
|
|
862
|
+
const absolutePath = path.join(root, relativePath)
|
|
863
|
+
try {
|
|
864
|
+
const stat = fs.lstatSync(absolutePath)
|
|
865
|
+
if (stat.isSymbolicLink()) continue
|
|
866
|
+
if (stat.isFile()) {
|
|
867
|
+
found.push(relativePath)
|
|
868
|
+
} else if (stat.isDirectory()) {
|
|
869
|
+
for (const entry of readDirectoryEntries(absolutePath).slice(0, MAX_CI_FILES)) {
|
|
870
|
+
if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) found.push(path.join(relativePath, entry.name))
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
} catch {}
|
|
874
|
+
}
|
|
875
|
+
const normalized = found.map(filename => filename.split(path.sep).join('/')).sort()
|
|
876
|
+
return {
|
|
877
|
+
found: normalized,
|
|
878
|
+
reviewRequired: normalized.length > 0,
|
|
879
|
+
reviewTargets: rankCiReviewTargets(normalized).slice(0, MAX_CI_REVIEW_TARGETS),
|
|
880
|
+
searched: CI_PATHS,
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Ranks likely test workflows first.
|
|
886
|
+
*
|
|
887
|
+
* @param {string[]} files discovered CI paths
|
|
888
|
+
* @returns {string[]} ranked paths
|
|
889
|
+
*/
|
|
890
|
+
function rankCiReviewTargets (files) {
|
|
891
|
+
return [...files].sort((left, right) => getCiRank(left) - getCiRank(right) || left.localeCompare(right))
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Returns a lower-is-better CI review rank.
|
|
896
|
+
*
|
|
897
|
+
* @param {string} filename CI path
|
|
898
|
+
* @returns {number} rank
|
|
899
|
+
*/
|
|
900
|
+
function getCiRank (filename) {
|
|
901
|
+
const value = filename.toLowerCase()
|
|
902
|
+
if (/(?:test|ci|build|unit|integration)/.test(value)) return 0
|
|
903
|
+
return 10
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Finds the package manifest that owns a detection.
|
|
908
|
+
*
|
|
909
|
+
* @param {string} repositoryRoot repository root
|
|
910
|
+
* @param {string|undefined} location detected relative path
|
|
911
|
+
* @returns {{json: object, path: string}} package metadata
|
|
912
|
+
*/
|
|
913
|
+
function getDetectionPackageJson (repositoryRoot, location) {
|
|
914
|
+
const candidate = typeof location === 'string' && path.basename(location) === 'package.json'
|
|
915
|
+
? path.resolve(repositoryRoot, location)
|
|
916
|
+
: path.join(repositoryRoot, 'package.json')
|
|
917
|
+
const json = readJson(candidate)
|
|
918
|
+
if (json) return { json, path: candidate }
|
|
919
|
+
return {
|
|
920
|
+
json: readJson(path.join(repositoryRoot, 'package.json')) || {},
|
|
921
|
+
path: path.join(repositoryRoot, 'package.json'),
|
|
922
|
+
}
|
|
624
923
|
}
|
|
625
924
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
925
|
+
/**
|
|
926
|
+
* Safely reads a bounded directory.
|
|
927
|
+
*
|
|
928
|
+
* @param {string} directory directory path
|
|
929
|
+
* @returns {fs.Dirent[]} entries
|
|
930
|
+
*/
|
|
931
|
+
function readDirectoryEntries (directory) {
|
|
633
932
|
try {
|
|
634
|
-
return fs.readdirSync(
|
|
933
|
+
return fs.readdirSync(directory, { withFileTypes: true }).slice(0, MAX_DIRECTORY_ENTRIES)
|
|
635
934
|
} catch {
|
|
636
935
|
return []
|
|
637
936
|
}
|
|
638
937
|
}
|
|
639
938
|
|
|
640
939
|
/**
|
|
641
|
-
*
|
|
940
|
+
* Reads bounded UTF-8 source.
|
|
642
941
|
*
|
|
643
|
-
* @param {string}
|
|
644
|
-
* @
|
|
645
|
-
* @returns {string} absolute package.json path
|
|
942
|
+
* @param {string} filename file path
|
|
943
|
+
* @returns {string|undefined} source
|
|
646
944
|
*/
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
945
|
+
function readText (filename) {
|
|
946
|
+
try {
|
|
947
|
+
const stat = fs.lstatSync(filename)
|
|
948
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE_BYTES) return
|
|
949
|
+
return fs.readFileSync(filename, 'utf8')
|
|
950
|
+
} catch {}
|
|
650
951
|
}
|
|
651
952
|
|
|
652
953
|
/**
|
|
653
|
-
*
|
|
954
|
+
* Reads JSON without throwing.
|
|
654
955
|
*
|
|
655
|
-
* @param {string}
|
|
656
|
-
* @
|
|
657
|
-
* @param {object} packageJson detected project package.json
|
|
658
|
-
* @returns {string|null} installed or declared framework version
|
|
956
|
+
* @param {string} filename JSON path
|
|
957
|
+
* @returns {object|undefined} parsed object
|
|
659
958
|
*/
|
|
660
|
-
function
|
|
661
|
-
|
|
959
|
+
function readJson (filename) {
|
|
960
|
+
const source = readText(filename)
|
|
961
|
+
if (source === undefined) return
|
|
662
962
|
try {
|
|
663
|
-
|
|
664
|
-
return readJson(filename)?.version || null
|
|
963
|
+
return JSON.parse(source)
|
|
665
964
|
} catch {}
|
|
965
|
+
}
|
|
666
966
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
967
|
+
/**
|
|
968
|
+
* Returns whether a framework target was selected.
|
|
969
|
+
*
|
|
970
|
+
* @param {string} framework framework id
|
|
971
|
+
* @param {Set<string>} selected selected ids
|
|
972
|
+
* @returns {boolean} selection result
|
|
973
|
+
*/
|
|
974
|
+
function isSelected (framework, selected) {
|
|
975
|
+
return selected.size === 0 || selected.has(framework)
|
|
671
976
|
}
|
|
672
977
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
}
|
|
978
|
+
/**
|
|
979
|
+
* Returns a stable project identifier.
|
|
980
|
+
*
|
|
981
|
+
* @param {object} packageJson package metadata
|
|
982
|
+
* @param {string} projectRoot project root
|
|
983
|
+
* @param {string} repositoryRoot repository root
|
|
984
|
+
* @returns {string} project id
|
|
985
|
+
*/
|
|
986
|
+
function getProjectId (packageJson, projectRoot, repositoryRoot) {
|
|
987
|
+
const value = packageJson.name || path.relative(repositoryRoot, projectRoot) || 'root'
|
|
988
|
+
return value.replace(/^@/, '').replaceAll(/[^A-Za-z0-9_.-]+/g, '-')
|
|
694
989
|
}
|
|
695
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Detects package-manager metadata for reporting only.
|
|
993
|
+
*
|
|
994
|
+
* @param {string} root repository root
|
|
995
|
+
* @returns {string} package manager
|
|
996
|
+
*/
|
|
696
997
|
function detectPackageManager (root) {
|
|
697
998
|
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
698
999
|
if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn'
|
|
699
|
-
return 'npm'
|
|
1000
|
+
if (fs.existsSync(path.join(root, 'package-lock.json'))) return 'npm'
|
|
1001
|
+
return 'unknown'
|
|
700
1002
|
}
|
|
701
1003
|
|
|
1004
|
+
/**
|
|
1005
|
+
* Detects workspace metadata for reporting only.
|
|
1006
|
+
*
|
|
1007
|
+
* @param {string} root repository root
|
|
1008
|
+
* @returns {string} workspace manager
|
|
1009
|
+
*/
|
|
702
1010
|
function detectWorkspaceManager (root) {
|
|
703
1011
|
if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))) return 'pnpm'
|
|
704
1012
|
const packageJson = readJson(path.join(root, 'package.json'))
|
|
705
1013
|
return packageJson?.workspaces ? detectPackageManager(root) : 'none'
|
|
706
1014
|
}
|
|
707
1015
|
|
|
1016
|
+
/**
|
|
1017
|
+
* Returns a manifest operating-system name.
|
|
1018
|
+
*
|
|
1019
|
+
* @param {string} platform Node.js platform
|
|
1020
|
+
* @returns {string} OS name
|
|
1021
|
+
*/
|
|
708
1022
|
function getManifestOs (platform) {
|
|
709
|
-
|
|
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, '-')
|
|
1023
|
+
return platform === 'win32' ? 'windows' : platform
|
|
725
1024
|
}
|
|
726
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Checks lexical path containment.
|
|
1028
|
+
*
|
|
1029
|
+
* @param {string} root root path
|
|
1030
|
+
* @param {string} filename candidate path
|
|
1031
|
+
* @returns {boolean} whether the candidate is contained
|
|
1032
|
+
*/
|
|
727
1033
|
function isPathInside (root, filename) {
|
|
728
|
-
const relative = path.relative(root, filename)
|
|
729
|
-
return relative
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function readJson (filename) {
|
|
733
|
-
try {
|
|
734
|
-
return JSON.parse(fs.readFileSync(filename, 'utf8'))
|
|
735
|
-
} catch {}
|
|
1034
|
+
const relative = path.relative(path.resolve(root), path.resolve(filename))
|
|
1035
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
736
1036
|
}
|
|
737
1037
|
|
|
738
1038
|
module.exports = { createManifestScaffold }
|