dd-trace 6.21.0 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  16. package/index.d.ts +25 -14
  17. package/initialize.mjs +1 -3
  18. package/openfeature/rules-based.d.ts +11 -0
  19. package/openfeature/rules-based.js +7 -0
  20. package/package.json +12 -10
  21. package/packages/datadog-core/src/storage.js +16 -0
  22. package/packages/datadog-esbuild/index.js +18 -12
  23. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  24. package/packages/datadog-instrumentations/src/ai.js +3 -3
  25. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  26. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  27. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  28. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  29. package/packages/datadog-instrumentations/src/cucumber.js +48 -33
  30. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  31. package/packages/datadog-instrumentations/src/electron.js +6 -6
  32. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  33. package/packages/datadog-instrumentations/src/fs.js +2 -2
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  35. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  36. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
  37. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  38. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  39. package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
  40. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  41. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
  42. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  47. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  48. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  49. package/packages/datadog-instrumentations/src/jest.js +3 -10
  50. package/packages/datadog-instrumentations/src/knex.js +9 -5
  51. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  52. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  53. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  54. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -2
  55. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
  56. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  57. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  58. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  59. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  60. package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
  61. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  62. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  63. package/packages/datadog-instrumentations/src/openai.js +7 -0
  64. package/packages/datadog-instrumentations/src/pino.js +5 -6
  65. package/packages/datadog-instrumentations/src/redis.js +27 -13
  66. package/packages/datadog-instrumentations/src/router.js +30 -0
  67. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  68. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
  69. package/packages/datadog-instrumentations/src/vitest-main.js +9 -15
  70. package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
  71. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  72. package/packages/datadog-instrumentations/src/vitest.js +2 -2
  73. package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
  74. package/packages/datadog-instrumentations/src/ws.js +1 -4
  75. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  76. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  77. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  78. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  79. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  80. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  82. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  83. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  84. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  85. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  86. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -5
  87. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  88. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  89. package/packages/datadog-plugin-cypress/src/support.js +1 -4
  90. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  91. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  92. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  93. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  94. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  95. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  96. package/packages/datadog-plugin-mocha/src/index.js +7 -10
  97. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
  98. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  99. package/packages/datadog-plugin-next/src/index.js +1 -2
  100. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  101. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  102. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  103. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  104. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  105. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  106. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  107. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  108. package/packages/datadog-webpack/index.js +55 -43
  109. package/packages/datadog-webpack/src/loader.js +56 -8
  110. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  111. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  112. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  113. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  114. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  115. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  116. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  117. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  118. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  119. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  120. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  121. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  122. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  123. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  124. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  125. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  126. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  127. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  128. package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
  129. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  132. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  133. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  134. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  135. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  136. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  137. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  138. package/packages/dd-trace/src/config/defaults.js +5 -5
  139. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
  140. package/packages/dd-trace/src/config/helper.js +34 -3
  141. package/packages/dd-trace/src/config/index.js +10 -9
  142. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  143. package/packages/dd-trace/src/config/parsers.js +2 -2
  144. package/packages/dd-trace/src/config/supported-configurations.json +35 -0
  145. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  146. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  147. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
  148. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  149. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  150. package/packages/dd-trace/src/dogstatsd.js +1 -1
  151. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  152. package/packages/dd-trace/src/exporters/common/request.js +1 -5
  153. package/packages/dd-trace/src/id.js +14 -1
  154. package/packages/dd-trace/src/lambda/handler.js +179 -19
  155. package/packages/dd-trace/src/lambda/index.js +15 -1
  156. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  157. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +12 -0
  159. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  160. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  161. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  162. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  163. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  165. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
  166. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  167. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  168. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  169. package/packages/dd-trace/src/llmobs/span_processor.js +1 -2
  170. package/packages/dd-trace/src/llmobs/tagger.js +2 -2
  171. package/packages/dd-trace/src/llmobs/util.js +1 -2
  172. package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
  173. package/packages/dd-trace/src/log/log.js +1 -5
  174. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  175. package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
  176. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  177. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  179. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  180. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
  181. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  182. package/packages/dd-trace/src/otel-sampling.js +4 -5
  183. package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
  184. package/packages/dd-trace/src/plugins/database.js +7 -16
  185. package/packages/dd-trace/src/plugins/util/git.js +9 -25
  186. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  187. package/packages/dd-trace/src/plugins/util/test.js +48 -4
  188. package/packages/dd-trace/src/priority_sampler.js +1 -2
  189. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  190. package/packages/dd-trace/src/proxy.js +1 -2
  191. package/packages/dd-trace/src/ritm.js +4 -2
  192. package/packages/dd-trace/src/serverless.js +5 -0
  193. package/packages/dd-trace/src/span_processor.js +13 -17
  194. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  195. package/packages/dd-trace/src/util.js +29 -0
  196. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  197. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  198. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  199. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  200. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  201. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  202. package/vendor/dist/protobufjs/index.js +1 -1
  203. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  204. package/vendor/dist/shell-quote/index.js +1 -1
@@ -1055,9 +1055,9 @@ function createWrapCallbackGetConnection (options) {
1055
1055
  return function wrapGetConnection (getConnection) {
1056
1056
  return function () {
1057
1057
  const callback = arguments[arguments.length - 1]
1058
- if (typeof callback !== 'function') return getConnection.apply(this, arguments)
1059
-
1060
- if (!connectionStartCh.hasSubscribers) return getConnection.apply(this, arguments)
1058
+ if (typeof callback !== 'function' || !connectionStartCh.hasSubscribers) {
1059
+ return getConnection.apply(this, arguments)
1060
+ }
1061
1061
 
1062
1062
  if (!acquireStartCh.hasSubscribers) {
1063
1063
  const ctx = {}
@@ -11,33 +11,45 @@ const MINIMUM_MOCHA_VERSION = DD_MAJOR >= 6 ? '>=8.0.0' : '>=5.2.0'
11
11
  const parameterizedTestCh = channel('ci:mocha:test:parameterize')
12
12
  const patched = new WeakSet()
13
13
 
14
+ /** @typedef {{ moduleBaseDir?: string }} MochaHookMeta */
15
+
14
16
  /**
15
17
  * Registers every Mocha package view that can expose the constructor.
16
18
  *
17
19
  * @param {string[]} versions
18
- * @param {(Mocha: Function, frameworkVersion: string) => Function} wrapMochaRun
20
+ * @param {(Mocha: Function, frameworkVersion: string, hookMeta: MochaHookMeta) => Function} wrapMochaRun
19
21
  */
20
22
  function addMochaRunHooks (versions, wrapMochaRun) {
21
- const patchedMochaConstructors = new WeakSet()
23
+ const patchedMochaConstructors = new WeakMap()
22
24
 
23
25
  /**
24
26
  * @param {Function} Mocha
25
27
  * @param {string} frameworkVersion
28
+ * @param {boolean | undefined} isIitm
29
+ * @param {{ moduleBaseDir: string } | undefined} hookMeta
26
30
  * @returns {Function}
27
31
  */
28
- function wrapMochaOnce (Mocha, frameworkVersion) {
29
- if (patchedMochaConstructors.has(Mocha)) return Mocha
30
- patchedMochaConstructors.add(Mocha)
31
- return wrapMochaRun(Mocha, frameworkVersion)
32
+ function wrapMochaOnce (Mocha, frameworkVersion, isIitm, hookMeta) {
33
+ let metadata = patchedMochaConstructors.get(Mocha)
34
+ if (metadata) {
35
+ // A bundler can expose the constructor before an ordinary loader supplies its package directory.
36
+ metadata.moduleBaseDir ??= hookMeta?.moduleBaseDir
37
+ return Mocha
38
+ }
39
+ metadata = { moduleBaseDir: hookMeta?.moduleBaseDir }
40
+ patchedMochaConstructors.set(Mocha, metadata)
41
+ return wrapMochaRun(Mocha, frameworkVersion, metadata)
32
42
  }
33
43
 
34
44
  /**
35
45
  * @param {object|Function} MochaPackage
36
46
  * @param {string} frameworkVersion
47
+ * @param {boolean | undefined} isIitm
48
+ * @param {{ moduleBaseDir: string } | undefined} hookMeta
37
49
  * @returns {object|Function}
38
50
  */
39
- function wrapMochaPackage (MochaPackage, frameworkVersion) {
40
- wrapMochaOnce(MochaPackage.default ?? MochaPackage, frameworkVersion)
51
+ function wrapMochaPackage (MochaPackage, frameworkVersion, isIitm, hookMeta) {
52
+ wrapMochaOnce(MochaPackage.default ?? MochaPackage, frameworkVersion, isIitm, hookMeta)
41
53
  return MochaPackage
42
54
  }
43
55
 
@@ -1,6 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { AsyncResource } = require('node:async_hooks')
4
+ const { realpathSync } = require('node:fs')
5
+ const { resolve, sep } = require('node:path')
6
+ const { fileURLToPath } = require('node:url')
4
7
 
5
8
  const { createCoverageMap } = require('../../../../vendor/dist/istanbul-lib-coverage')
6
9
  const satisfies = require('../../../../vendor/dist/semifies')
@@ -16,6 +19,7 @@ const { EMPTY_EFD_RETRY_POLICY } = require('../../../dd-trace/src/ci-visibility/
16
19
  const { writeCoverageBackfillToCache } = require('../../../dd-trace/src/ci-visibility/test-optimization-cache')
17
20
  const log = require('../../../dd-trace/src/log')
18
21
  const { getEnvironmentVariable } = require('../../../dd-trace/src/config/helper')
22
+ const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace')
19
23
  const {
20
24
  getTestSuitePath,
21
25
  MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
@@ -88,6 +92,9 @@ const runnerRecoveryStates = new WeakMap()
88
92
  const runnersWithPendingCoverageReset = new WeakSet()
89
93
  const parallelRunners = new WeakSet()
90
94
  const wrappedRunnerEmitPrototypes = new WeakSet()
95
+ const rootSuitesWithHookFiles = new WeakSet()
96
+ const ROOT_HOOK_NAMES = ['_beforeAll', '_afterAll', '_beforeEach', '_afterEach']
97
+ const ROOT_SUITE_ARRAY_NAMES = ['tests', 'suites', '_onlyTests', '_onlySuites', ...ROOT_HOOK_NAMES]
91
98
  let hasWarnedDeprecatedMochaVersion = false
92
99
 
93
100
  const unskippableSuites = []
@@ -169,6 +176,40 @@ function haveRootTestsFinished (rootTests) {
169
176
  return true
170
177
  }
171
178
 
179
+ /**
180
+ * @param {{ file: string }} test
181
+ */
182
+ function startRootSuite (test) {
183
+ const ctx = testFileToSuiteCtx.get(test.file)
184
+ if (ctx?._pendingRootStart) {
185
+ ctx._pendingRootStart = false
186
+ testSuiteStartCh.runStores(ctx, () => {})
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {string} file
192
+ * @param {'pass' | 'fail' | 'skip'} status
193
+ * @param {object} [runner] Defers coverage reset until this runner's suite-end reporters finish.
194
+ */
195
+ function finishRootSuite (file, status, runner) {
196
+ const ctx = testFileToSuiteCtx.get(file)
197
+ if (!ctx) {
198
+ log.warn('No ctx found for suite', file)
199
+ return
200
+ }
201
+
202
+ if (global.__coverage__) {
203
+ const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
204
+ testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
205
+ mergeCoverage(global.__coverage__, originalCoverageMap)
206
+ if (runner) runnersWithPendingCoverageReset.add(runner)
207
+ else resetCoverage(global.__coverage__)
208
+ }
209
+
210
+ testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
211
+ }
212
+
172
213
  function getSuitePath (suite) {
173
214
  return getTestSuitePath(suite.file, process.cwd())
174
215
  }
@@ -931,6 +972,23 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
931
972
  return
932
973
  }
933
974
 
975
+ if (runner._opts?.cleanReferencesAfterRun === false) {
976
+ const rootSuite = runner.suite
977
+ const originalArrays = ROOT_SUITE_ARRAY_NAMES.map(name => rootSuite[name])
978
+ // Restore definitions before completion callbacks can start another run, including with the plugin disabled.
979
+ runner.prependOnceListener('end', () => {
980
+ for (let index = 0; index < ROOT_SUITE_ARRAY_NAMES.length; index++) {
981
+ rootSuite[ROOT_SUITE_ARRAY_NAMES[index]] = originalArrays[index]
982
+ }
983
+ })
984
+ }
985
+
986
+ // Root hooks must be selected before .only() removes other files from the test tree.
987
+ for (const name of ROOT_HOOK_NAMES) {
988
+ runner.suite[name] = runner.suite[name].filter(hook => !hook.file ||
989
+ !suitesToSkip.includes(getSuitePath(hook)) || unskippableSuites.includes(hook.file))
990
+ }
991
+
934
992
  // We remove the suites that we skip through ITR
935
993
  // Mocha normally applies exclusivity after this asynchronous configuration step.
936
994
  if (typeof runner.suite.hasOnly === 'function') {
@@ -940,17 +998,28 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
940
998
  }
941
999
  const numTestsToRun = runner.grepTotal(runner.suite)
942
1000
  const filteredSuites = getFilteredSuites(runner.suite.suites)
1001
+ // Root tests share a Mocha suite across files, but TIA still selects them by file.
1002
+ const filteredRootTests = getFilteredSuites(runner.suite.tests)
943
1003
  const { suitesToRun, suitesToSkipForRun } = filteredSuites
944
1004
 
945
- isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length
1005
+ isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length ||
1006
+ filteredRootTests.suitesToRun.length !== runner.suite.tests.length
946
1007
 
947
1008
  log.debug('%d out of %d suites are going to run.', suitesToRun.length, runner.suite.suites.length)
948
1009
 
949
1010
  runner.suite.suites = suitesToRun
1011
+ runner.suite.tests = filteredRootTests.suitesToRun
1012
+ // Mocha reapplies exclusivity when the delayed runner resumes.
1013
+ if (runner.suite._onlyTests.length) {
1014
+ runner.suite._onlyTests = runner.suite.tests
1015
+ }
1016
+ if (runner.suite._onlySuites.length) {
1017
+ runner.suite._onlySuites = runner.suite._onlySuites.filter(suite => suitesToRun.includes(suite))
1018
+ }
950
1019
  areAllSuitesSkipped = isSuitesSkipped && numTestsToRun > 0 && runner.grepTotal(runner.suite) === 0
951
1020
 
952
- skippedSuites = [...filteredSuites.skippedSuites]
953
- suitesToSkip = suitesToSkipForRun
1021
+ skippedSuites = [...new Set([...filteredSuites.skippedSuites, ...filteredRootTests.skippedSuites])]
1022
+ suitesToSkip = [...new Set([...suitesToSkipForRun, ...filteredRootTests.suitesToSkipForRun])]
954
1023
  skippedSuitesCoverage = getSkippedSuitesCoverageForRun()
955
1024
  writeCoverageBackfillToCache(skippedSuitesCoverage, getCoverageRootDir())
956
1025
 
@@ -1095,11 +1164,51 @@ function runMochaRunner (run, runner, args) {
1095
1164
  /**
1096
1165
  * @param {Function} Mocha
1097
1166
  * @param {string} frameworkVersion
1167
+ * @param {import('./common').MochaHookMeta} hookMeta
1098
1168
  * @returns {Function}
1099
1169
  */
1100
- function wrapMochaRun (Mocha, frameworkVersion) {
1170
+ function wrapMochaRun (Mocha, frameworkVersion, hookMeta) {
1101
1171
  warnDeprecatedMochaVersion(frameworkVersion)
1102
1172
 
1173
+ // Shimmer is required to attach listeners to the root suite created by each Mocha instance.
1174
+ shimmer.wrap(Mocha.prototype, 'ui', ui => function (...args) {
1175
+ const result = ui.apply(this, args)
1176
+ const suite = this.suite
1177
+ if (!rootSuitesWithHookFiles.has(suite)) {
1178
+ rootSuitesWithHookFiles.add(suite)
1179
+ let file
1180
+ suite.prependListener('pre-require', (context, testFile) => {
1181
+ file = testFile
1182
+ })
1183
+ suite.on('post-require', () => {
1184
+ file = undefined
1185
+ })
1186
+ // Hook objects are created at runtime; inspect the caller while registration is still on the stack.
1187
+ shimmer.wrap(suite, '_createHook', createHook => function createHookWithFile (...args) {
1188
+ const hook = createHook.apply(this, args)
1189
+ if (file && !hook.file) {
1190
+ try {
1191
+ const { moduleBaseDir } = hookMeta
1192
+ const mochaPathPrefix = moduleBaseDir && moduleBaseDir + sep
1193
+ for (const site of getCallSites(createHookWithFile)) {
1194
+ const source = site.getFileName()
1195
+ if (!source || source.startsWith('node:')) continue
1196
+ const sourceFile = source.startsWith('file:') ? fileURLToPath(source) : source
1197
+ if (mochaPathPrefix && sourceFile.startsWith(mochaPathPrefix)) continue
1198
+ // Helpers register shared setup; only direct declarations belong to the loading test file.
1199
+ if (sourceFile === file || realpathSync(sourceFile) === realpathSync(file)) hook.file = file
1200
+ break
1201
+ }
1202
+ } catch (error) {
1203
+ log.debug('Error capturing Mocha root hook source: %s', error)
1204
+ }
1205
+ }
1206
+ return hook
1207
+ })
1208
+ }
1209
+ return result
1210
+ })
1211
+
1103
1212
  // Shimmer is required because run must return its Runner while execution is paused and resumed after configuration.
1104
1213
  shimmer.wrap(Mocha.prototype, 'run', run => function (...args) {
1105
1214
  // Workers do not need to request any data, just run the tests
@@ -1123,7 +1232,7 @@ function wrapMochaRun (Mocha, frameworkVersion) {
1123
1232
  this.files.forEach((path) => {
1124
1233
  const isUnskippable = isMarkedAsUnskippable({ path })
1125
1234
  if (isUnskippable) {
1126
- unskippableSuites.push(path)
1235
+ unskippableSuites.push(resolve(path))
1127
1236
  }
1128
1237
  })
1129
1238
 
@@ -1200,8 +1309,7 @@ addHook({
1200
1309
  filePattern: String.raw`lib/runner\.(?:c?js)$`,
1201
1310
  }, function (runnerPackage, frameworkVersion) {
1202
1311
  const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
1203
- if (typeof Runner !== 'function') return
1204
- if (patched.has(Runner)) return
1312
+ if (typeof Runner !== 'function' || patched.has(Runner)) return
1205
1313
 
1206
1314
  patched.add(Runner)
1207
1315
  wrapRunnerEmit(Runner)
@@ -1226,12 +1334,12 @@ addHook({
1226
1334
  runnerFrameworkErrors.delete(this)
1227
1335
  }
1228
1336
 
1229
- const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
1337
+ let { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
1230
1338
  // Root-level tests (direct children of root, no describe wrapper) keyed by file.
1231
1339
  // Populated during the root 'suite' event so the normal finish path can include them
1232
1340
  // in mixed-file status calculation.
1233
1341
  const rootTestsByFile = new Map()
1234
- // Counts how many original tests per pure-root file still need their final attempt.
1342
+ // Counts how many selected tests per pure-root file still need their final attempt.
1235
1343
  // Hits zero when the last test's lifecycle completes, triggering the suite finish.
1236
1344
  const rootPendingCountByFile = new Map()
1237
1345
  const rootFinalizationPendingCountByFile = new Map()
@@ -1304,33 +1412,18 @@ addHook({
1304
1412
  }
1305
1413
  rootPendingCountByFile.delete(file)
1306
1414
 
1307
- const ctx = testFileToSuiteCtx.get(file)
1308
- if (!ctx) {
1309
- log.warn('No ctx found for suite', file)
1310
- return
1311
- }
1312
-
1313
1415
  const rootTests = rootTestsByFile.get(file) || []
1314
- const status = getRootSuiteStatus(rootTests)
1315
-
1316
- if (global.__coverage__) {
1317
- const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
1318
- testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
1319
- mergeCoverage(global.__coverage__, originalCoverageMap)
1320
- resetCoverage(global.__coverage__)
1321
- }
1322
-
1323
- testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
1416
+ finishRootSuite(file, getRootSuiteStatus(rootTests))
1324
1417
  }
1325
1418
 
1326
- function finishRootSuiteFallbackForFile (file) {
1419
+ function finishRootSuiteFallbackForFile (file, runner) {
1327
1420
  const ctx = testFileToSuiteCtx.get(file)
1328
1421
  if (!ctx || !rootPendingCountByFile.has(file)) return
1329
1422
 
1330
1423
  const rootTests = rootTestsByFile.get(file) || []
1331
1424
  const status = haveRootTestsFinished(rootTests) ? getRootSuiteStatus(rootTests) : 'fail'
1332
1425
  rootPendingCountByFile.delete(file)
1333
- testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
1426
+ finishRootSuite(file, status, runner)
1334
1427
  }
1335
1428
 
1336
1429
  function finishRootSuiteAfterFinalAttempt (test) {
@@ -1361,13 +1454,7 @@ addHook({
1361
1454
  this.prependListener('test', getOnTestHandler(true))
1362
1455
  // Reporters are registered before Runner#run, so prepend this after the test handler
1363
1456
  // to start the suite first and preserve the test event if a reporter throws.
1364
- this.prependListener('test', function (test) {
1365
- const ctx = testFileToSuiteCtx.get(test.file)
1366
- if (ctx?._pendingRootStart) {
1367
- ctx._pendingRootStart = false
1368
- testSuiteStartCh.runStores(ctx, () => {})
1369
- }
1370
- })
1457
+ this.prependListener('test', startRootSuite)
1371
1458
 
1372
1459
  // Reporters are registered before this run wrapper. Prepend terminal handlers
1373
1460
  // so a reporter error cannot strand a test span that Mocha already completed.
@@ -1407,8 +1494,16 @@ addHook({
1407
1494
  this.prependListener('fail', getOnFailHandler(true, config))
1408
1495
 
1409
1496
  this.prependListener('pending', getOnPendingHandler())
1497
+ // Static skips emit 'pending' without 'test', so their suite must start here too.
1498
+ this.prependListener('pending', startRootSuite)
1410
1499
 
1411
1500
  this.prependListener('suite', function (suite) {
1501
+ if (suite.root) {
1502
+ // TIA and .only() have now selected the suites that can finish during this run.
1503
+ const selectedSuites = getSuitesByTestFile(suite)
1504
+ suitesByTestFile = selectedSuites.suitesByTestFile
1505
+ numSuitesByTestFile = selectedSuites.numSuitesByTestFile
1506
+ }
1412
1507
  if (suite.root || !suite.tests.length) {
1413
1508
  // This branch can be triggered when we have top level it(...) inside test files.
1414
1509
  // In that case, they all (even if they are from different files) are going to be
@@ -1418,9 +1513,14 @@ addHook({
1418
1513
  // below. Since 'suite' fires for root first, in the mixed case the ctx is created
1419
1514
  // here and the describe-based handler finds it already set.
1420
1515
  if (suite.root && suite.tests.length > 0) {
1516
+ const { _grep: grep, _invert: invert } = this
1517
+ const lastIndex = grep.lastIndex
1518
+ const selectedTests = suite.tests.filter(test => grep.test(test.fullTitle()) !== !!invert)
1519
+ // Inspecting a stateful grep must not change which tests Mocha subsequently selects.
1520
+ if (grep.lastIndex !== lastIndex) grep.lastIndex = lastIndex
1421
1521
  const files = new Set(suite.tests.map(test => test.file).filter(Boolean))
1422
1522
  for (const file of files) {
1423
- const testsForFile = suite.tests.filter(t => t.file === file)
1523
+ const testsForFile = selectedTests.filter(t => t.file === file)
1424
1524
  rootTestsByFile.set(file, testsForFile)
1425
1525
  // Only track the countdown for pure root-level files.
1426
1526
  // Mixed files are finished by the normal 'suite end' path.
@@ -1461,10 +1561,10 @@ addHook({
1461
1561
  this.prependListener('suite end', function (suite) {
1462
1562
  if (suite.root) {
1463
1563
  // Normal case: pure root-level files are finished by the 'test end' / 'hook end'
1464
- // listeners via finishRootSuiteForFile. Two edge cases remain here:
1564
+ // listeners via finishRootSuiteForFile. Fallback cases remain here:
1465
1565
  //
1466
- // 1. All-pending: no 'test' event fired, _pendingRootStart is still true.
1467
- // Start and immediately finish with 'skip'.
1566
+ // 1. Unstarted file: no 'test' or 'pending' event fired (e.g. all tests
1567
+ // were filtered out). Start and immediately finish with 'skip'.
1468
1568
  //
1469
1569
  // 2. Aborted mid-run (e.g. a beforeEach hook failure): Mocha skips remaining
1470
1570
  // tests and jumps straight to 'suite end'. rootPendingCountByFile still has
@@ -1491,7 +1591,7 @@ addHook({
1491
1591
  continue
1492
1592
  }
1493
1593
 
1494
- finishRootSuiteFallbackForFile(test.file)
1594
+ finishRootSuiteFallbackForFile(test.file, this)
1495
1595
  }
1496
1596
  }
1497
1597
  return
@@ -1584,10 +1684,7 @@ addHook({
1584
1684
  file: 'src/WorkerHandler.js',
1585
1685
  }, (workerHandlerPackage) => {
1586
1686
  shimmer.wrap(workerHandlerPackage.prototype, 'exec', exec => function (_, path) {
1587
- if (!testFinishCh.hasSubscribers) {
1588
- return exec.apply(this, arguments)
1589
- }
1590
- if (!path?.length) {
1687
+ if (!testFinishCh.hasSubscribers || !path?.length) {
1591
1688
  return exec.apply(this, arguments)
1592
1689
  }
1593
1690
  const [testSuiteAbsolutePath] = path
@@ -899,6 +899,7 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
899
899
  const shouldFinishTest = ctx && (
900
900
  test._ddReporterStartFailed ||
901
901
  test._ddReporterTerminalFailed ||
902
+ !test.fn ||
902
903
  !getAfterEachHooks(test).length ||
903
904
  (test._ddIsDisabled && !test._ddIsAttemptToFix)
904
905
  )
@@ -906,8 +907,8 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
906
907
  let testFinishInfo
907
908
  let isFinalAttempt = false
908
909
 
909
- // If there are afterEach to be run, we don't finish the test yet.
910
- // Disabled tests (marked pending by us) are finished immediately without waiting for afterEach hooks.
910
+ // If there are afterEach hooks to run, we don't finish the test yet.
911
+ // Static skips have no function and never execute hooks. Disabled tests also finish immediately.
911
912
  // In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
912
913
  // getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
913
914
  // directly sets finalStatus without waiting for hooks
@@ -443,8 +443,7 @@ addHook({
443
443
  filePattern: String.raw`lib/runner\.(?:c?js)$`,
444
444
  }, function (runnerPackage) {
445
445
  const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
446
- if (typeof Runner !== 'function') return
447
- if (patchedRunners.has(Runner)) return
446
+ if (typeof Runner !== 'function' || patchedRunners.has(Runner)) return
448
447
 
449
448
  patchedRunners.add(Runner)
450
449
  shimmer.wrap(Runner.prototype, 'runTests', runTests => getRunTestsWrapper(runTests, config))
@@ -45,7 +45,7 @@ addHook({ name: 'mongodb', versions: ['>=4.6.0 <6.4.0'], file: 'lib/cmap/connect
45
45
 
46
46
  addHook({ name: 'mongodb', versions: ['>=6.4.0'], file: 'lib/cmap/connection.js' }, Connection => {
47
47
  const proto = Connection.Connection.prototype
48
- shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', undefined, instrumentPromise))
48
+ shimmer.wrap(proto, 'command', command => wrapConnectionCommand(command, 'command', instrumentPromise))
49
49
  })
50
50
 
51
51
  addHook({ name: 'mongodb', versions: ['>=3.3 <4'], file: 'lib/core/wireprotocol/index.js' }, wp => wrapWp(wp))
@@ -95,8 +95,8 @@ function wrapUnifiedCommand (command, operation, name) {
95
95
  }
96
96
  }
97
97
 
98
- function wrapConnectionCommand (command, operation, name, instrumentFn = instrument) {
99
- const opts = { name }
98
+ function wrapConnectionCommand (command, operation, instrumentFn = instrument) {
99
+ const opts = { name: undefined }
100
100
  return function (ns, ops) {
101
101
  if (!startCh.hasSubscribers) {
102
102
  return command.apply(this, arguments)
@@ -83,7 +83,7 @@ addHook({
83
83
 
84
84
  let callbackWrapped = false
85
85
 
86
- const wrapCallbackIfExist = (args, ctx) => {
86
+ const wrapCallbackIfExist = (args) => {
87
87
  const lastArgumentIndex = args.length - 1
88
88
 
89
89
  if (typeof args[lastArgumentIndex] === 'function') {
@@ -106,7 +106,7 @@ addHook({
106
106
  }
107
107
 
108
108
  return startCh.runStores(ctx, () => {
109
- wrapCallbackIfExist(args, ctx)
109
+ wrapCallbackIfExist(args)
110
110
 
111
111
  // A callback query executes synchronously inside `method.apply`, so the
112
112
  // exec scope (and the deferred driver call it spawns) must be entered
@@ -121,7 +121,7 @@ addHook({
121
121
  shimmer.wrap(res, 'exec', originalExec => {
122
122
  return function wrappedExec (...args) {
123
123
  if (!callbackWrapped) {
124
- wrapCallbackIfExist(args, ctx)
124
+ wrapCallbackIfExist(args)
125
125
  }
126
126
 
127
127
  const execResult = execCh.runStores(ctx, () => originalExec.apply(this, args))
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ const { LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES } = require('../../../dd-trace/src/llmobs/constants/audio')
4
+
5
+ // Leave room for the WAV header when the retained PCM is encoded.
6
+ const MAX_AUDIO = LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES - 128
7
+ const RATES = new Set([8000, 16_000, 24_000, 48_000])
8
+ const INVALID_BASE64 = /[^A-Za-z0-9+/]/
9
+
10
+ /** @param {object} configuration */
11
+ function pcmRate (configuration = {}) {
12
+ return configuration.mediaType === 'audio/lpcm' && configuration.sampleSizeBits === 16 &&
13
+ configuration.channelCount === 1 && configuration.encoding === 'base64' &&
14
+ RATES.has(configuration.sampleRateHertz)
15
+ ? configuration.sampleRateHertz
16
+ : 0
17
+ }
18
+
19
+ /** @param {string} encoded */
20
+ function decodedLength (encoded) {
21
+ if (typeof encoded !== 'string' || encoded.length % 4) return
22
+ const padding = encoded.endsWith('==') ? 2 : encoded.endsWith('=') ? 1 : 0
23
+ if (INVALID_BASE64.test(encoded.slice(0, encoded.length - padding))) return
24
+ return encoded.length / 4 * 3 - padding
25
+ }
26
+
27
+ /** A bounded, connection-wide sample timeline. Content-name changes do not reset its origin. */
28
+ class InputAudio {
29
+ rate = 0
30
+ valid = true
31
+ total = 0
32
+ anchor
33
+ #chunks = []
34
+ #bytes = 0
35
+
36
+ /** @param {object} configuration */
37
+ configure (configuration) {
38
+ const rate = pcmRate(configuration)
39
+ if (!rate || (this.total && rate !== this.rate)) this.valid = false
40
+ this.rate = rate
41
+ }
42
+
43
+ /**
44
+ * @param {string} encoded
45
+ * @param {number} now Milliseconds at SDK consumption, not an acknowledgement from the provider.
46
+ */
47
+ append (encoded, now) {
48
+ const length = decodedLength(encoded)
49
+ if (length === undefined) {
50
+ this.valid = false
51
+ this.clear()
52
+ return
53
+ }
54
+ if (length % 2) this.valid = false
55
+ this.total += length
56
+ if (this.rate) this.anchor = now - this.total * 1000 / (2 * this.rate)
57
+ if (!this.valid || !length) return
58
+ // Decode from a base64 quartet boundary, retaining at most two alignment bytes
59
+ // beyond the cap instead of allocating the entire incoming frame.
60
+ const start = Math.max(0, length - MAX_AUDIO)
61
+ const retained = Buffer.from(encoded.slice(Math.floor(start / 3) * 4), 'base64').subarray(start % 3)
62
+ this.#chunks.push(retained)
63
+ this.#bytes += retained.length
64
+ while (this.#bytes > MAX_AUDIO) {
65
+ const first = this.#chunks[0]
66
+ const excess = this.#bytes - MAX_AUDIO
67
+ if (first.length <= excess) {
68
+ this.#chunks.shift()
69
+ this.#bytes -= first.length
70
+ } else {
71
+ this.#chunks[0] = Buffer.from(first.subarray(excess))
72
+ this.#bytes -= excess
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {number} startMs
79
+ * @param {number} endMs
80
+ */
81
+ clip (startMs, endMs) {
82
+ if (!this.valid || !this.rate || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return
83
+ const start = Math.floor(startMs * this.rate / 1000) * 2
84
+ const end = Math.floor(endMs * this.rate / 1000) * 2
85
+ const base = this.total - this.#bytes
86
+ if (start < base || end > this.total) return
87
+ const clip = Buffer.allocUnsafe(end - start)
88
+ let offset = base
89
+ let written = 0
90
+ for (const chunk of this.#chunks) {
91
+ const chunkEnd = offset + chunk.length
92
+ if (chunkEnd > start) {
93
+ written += chunk.copy(clip, written, Math.max(start - offset, 0), Math.min(end - offset, chunk.length))
94
+ }
95
+ offset = chunkEnd
96
+ if (offset >= end) break
97
+ }
98
+ return clip
99
+ }
100
+
101
+ clear () {
102
+ this.#chunks = []
103
+ this.#bytes = 0
104
+ }
105
+ }
106
+
107
+ /** Ordered PCM playback projection for one response, including genuine underrun silence. */
108
+ class OutputAudio {
109
+ rate = 0
110
+ start
111
+ end
112
+ interrupted
113
+ generatedBytes = 0
114
+ timingValid = true
115
+ omittedReason
116
+ #chunks = []
117
+ #bytes = 0
118
+
119
+ /**
120
+ * @param {string} encoded
121
+ * @param {object} configuration
122
+ * @param {number} now
123
+ */
124
+ append (encoded, configuration, now) {
125
+ const length = decodedLength(encoded)
126
+ if (length !== undefined) this.generatedBytes += length
127
+ // Late chunks cannot resurrect playback or invalidate the clip already cut at interruption.
128
+ if (this.interrupted !== undefined) return
129
+ const rate = pcmRate(configuration)
130
+ if (length === undefined || !rate || length % 2 || (this.rate && this.rate !== rate)) {
131
+ this.timingValid = false
132
+ this.omittedReason = 'invalid_audio'
133
+ this.#chunks = []
134
+ this.#bytes = 0
135
+ return
136
+ }
137
+ if (!length) return
138
+ this.rate ||= rate
139
+ this.start ??= now
140
+ const previousEnd = this.end ?? now
141
+ const gap = Math.floor(Math.max(0, now - previousEnd) * rate / 1000) * 2
142
+ this.end = previousEnd + (gap + length) * 1000 / (2 * rate)
143
+ if (!this.omittedReason && this.#bytes + gap + length <= MAX_AUDIO) {
144
+ if (gap) this.#chunks.push(Buffer.alloc(gap))
145
+ this.#chunks.push(Buffer.from(encoded, 'base64'))
146
+ this.#bytes += gap + length
147
+ } else {
148
+ this.omittedReason ||= 'retention_limit'
149
+ this.#chunks = []
150
+ this.#bytes = 0
151
+ }
152
+ }
153
+
154
+ /** @param {number} now */
155
+ interrupt (now) {
156
+ if (this.interrupted !== undefined) return
157
+ this.interrupted = now
158
+ if (this.start === undefined || this.end === undefined || !this.rate) return
159
+ const frames = Math.floor(Math.max(0, now - this.start) * this.rate / 1000)
160
+ this.end = Math.min(this.end, this.start + frames * 1000 / this.rate)
161
+ if (this.#bytes > frames * 2) {
162
+ const clip = Buffer.concat(this.#chunks, this.#bytes).subarray(0, frames * 2)
163
+ this.#chunks = [Buffer.from(clip)]
164
+ this.#bytes = clip.length
165
+ }
166
+ }
167
+
168
+ pcm () {
169
+ if (!this.omittedReason && this.#bytes) return Buffer.concat(this.#chunks, this.#bytes)
170
+ }
171
+ }
172
+
173
+ module.exports = { InputAudio, OutputAudio }