From 45b0d9382206cf62f02536313b52645b3bb99c16 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:03:01 +0200 Subject: [PATCH 01/17] chore: prep 2.1.0 (#468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare `develop` for the next minor release. - bump version `2.0.1` → `2.1.0` - add `## Version 2.1.0 - tbd` changelog skeleton (Added / Changed / Fixed) Feature PRs merging into `develop` add their bullet under this section; the date is set when `develop` → `main` is promoted. --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72acf229..30c2c03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## Version 2.1.0 - tbd + +### Added + +### Changed + +### Fixed + ## Version 2.0.1 - 2026-07-03 ### Fixed diff --git a/package.json b/package.json index 08d883a4..a6291419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "description": "CDS plugin providing observability features, incl. automatic OpenTelemetry instrumentation.", "repository": { "type": "git", From 3e9d3161b07089b839c1f05867c28d64f8f40e00 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:15:57 +0200 Subject: [PATCH 02/17] ci: run on pull requests to develop (#469) Add `develop` to the CI workflow's `push` and `pull_request` branch triggers so PRs into `develop` (the new integration branch) get lint + the test matrix. `main` triggers unchanged. HANA remains `workflow_dispatch`-only. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df995b1d..79da765d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,9 @@ permissions: on: workflow_dispatch: push: - branches: [main] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] env: NPM_CONFIG_IGNORE_SCRIPTS: true From 29d002883f00151907ac18da0e96c004efa639fb Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:54:10 +0200 Subject: [PATCH 03/17] fix: trace Cloud SDK outbound requests (getter-only exports) (#451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Cloud SDK v4 exposes `executeHttpRequest` / `executeHttpRequestWithOrigin` as **getter-only properties**. The plugin patched them with plain assignment, which silently fails on a getter — so the Cloud SDK outbound path (CAP's default when `@sap-cloud-sdk/http-client` is installed) produced **no CLIENT span** and no `sap.btp.destination`. ## Fix Patch via `Object.defineProperty(cloudSDK, name, { value, writable: true, configurable: true })` — `writable`/`configurable` keep the exports re-patchable. Verified the wrapper now fires end-to-end. ## Tests - `test/tracing-remote-cloudsdk.test.js` — Cloud SDK path: asserts a `@cap-js/telemetry` CLIENT span with `sap.btp.destination`, and no undici span. - `test/tracing-remote-native.test.js` — native-fetch path: asserts the span comes from `@opentelemetry/instrumentation-undici` (not `-http`) with `http.*`/`url.*`/`server.*` attributes. Both drive a real local HTTP call; gated on `cds.version >= 9`. Changelog updated. --------- Co-authored-by: sjvans --- CHANGELOG.md | 2 + lib/tracing/cloud_sdk.js | 33 +- package-lock.json | 1276 ++++++++++++++++++++------ package.json | 1 + test/tracing-attributes.test.js | 2 +- test/tracing-remote-cloudsdk.test.js | 61 ++ test/tracing-remote-native.test.js | 57 ++ 7 files changed, 1151 insertions(+), 281 deletions(-) create mode 100644 test/tracing-remote-cloudsdk.test.js create mode 100644 test/tracing-remote-native.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c2c03f..3995a012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed +- Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) + ## Version 2.0.1 - 2026-07-03 ### Fixed diff --git a/lib/tracing/cloud_sdk.js b/lib/tracing/cloud_sdk.js index 792472a4..adc465fe 100644 --- a/lib/tracing/cloud_sdk.js +++ b/lib/tracing/cloud_sdk.js @@ -15,7 +15,10 @@ function _cloudSdkSpanName(destination, requestConfig) { return `${method}${path ? ' ' + path : ''}` } -// REVISIT: unverified! +// Instruments @sap-cloud-sdk/http-client's exports (the outbound path CAP uses by +// default when the cloud sdk is installed) to emit a CLIENT span per remote call. +// Note: cloud sdk v4 exposes executeHttpRequest(WithOrigin) as getter-only properties, +// so a plain assignment silently fails -> we must use Object.defineProperty with a value. module.exports = () => { try { require.resolve('@sap-cloud-sdk/http-client') @@ -25,26 +28,22 @@ module.exports = () => { const cloudSDK = require('@sap-cloud-sdk/http-client') const { executeHttpRequest: _execute, executeHttpRequestWithOrigin: _executeWithOrigin } = cloudSDK - cloudSDK.executeHttpRequest = wrap(_execute, { + const _executeHttpRequest = wrap(_execute, { wrapper: function executeHttpRequest(destination, requestConfig) { - return trace( - _cloudSdkSpanName(destination, requestConfig), - _execute, - this, - arguments, - { kind: SpanKind.CLIENT, outbound: destination.name } - ) + return trace(_cloudSdkSpanName(destination, requestConfig), _execute, this, arguments, { + kind: SpanKind.CLIENT, + outbound: destination.name + }) } }) - cloudSDK.executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { + Object.defineProperty(cloudSDK, 'executeHttpRequest', { value: _executeHttpRequest, writable: true, configurable: true }) + const _executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { wrapper: function executeHttpRequestWithOrigin(destination, requestConfig) { - return trace( - _cloudSdkSpanName(destination, requestConfig), - _executeWithOrigin, - this, - arguments, - { kind: SpanKind.CLIENT, outbound: destination.name } - ) + return trace(_cloudSdkSpanName(destination, requestConfig), _executeWithOrigin, this, arguments, { + kind: SpanKind.CLIENT, + outbound: destination.name + }) } }) + Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { value: _executeHttpRequestWithOrigin, writable: true, configurable: true }) } diff --git a/package-lock.json b/package-lock.json index 79d0248e..434f7a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "dev": true, "packages": { "": { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -33,6 +33,7 @@ "@opentelemetry/exporter-trace-otlp-proto": "^0.219", "@opentelemetry/instrumentation-host-metrics": "^0.2.0", "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", @@ -600,6 +601,28 @@ "resolved": "", "link": true }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -635,9 +658,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -692,9 +715,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -879,91 +902,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1481,10 +1419,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1912,6 +1849,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", @@ -1966,6 +1916,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", @@ -2001,18 +1964,6 @@ "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", - "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", @@ -2102,6 +2053,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", @@ -2205,6 +2169,19 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", @@ -2399,16 +2376,74 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@sap-cloud-sdk/connectivity": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.8.0.tgz", + "integrity": "sha512-y8H0AKgDecm7+A8wxx+J1oxJTzYEs+4Hy7ghfrBD57e9MdRRFI5FryYqokZbDcMjEnsbvZ+giShm/A2UXmeHyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/resilience": "^4.8.0", + "@sap-cloud-sdk/util": "^4.8.0", + "@sap/xsenv": "^6.2.0", + "@sap/xssec": "^4.13.0", + "async-retry": "^1.3.3", + "axios": "^1.15.0", + "jks-js": "^1.1.6", + "jsonwebtoken": "^9.0.3", + "safe-stable-stringify": "^2.5.0" + } + }, + "node_modules/@sap-cloud-sdk/http-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/http-client/-/http-client-4.8.0.tgz", + "integrity": "sha512-ruQd7d0nObshm0fNkhUE1fp1qxevEYxblhM/X0rG1goN9r4nQiT6LgZUG5s+QeR1R2LdIHBXtsIycBfz/4kOog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/connectivity": "^4.8.0", + "@sap-cloud-sdk/resilience": "^4.8.0", + "@sap-cloud-sdk/util": "^4.8.0", + "axios": "^1.15.0" + } + }, + "node_modules/@sap-cloud-sdk/resilience": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.8.0.tgz", + "integrity": "sha512-/XclOtUHhdN39acKH1otJdMILAm+HJkQ6wjpUvqBx7RNiQ1GQwe34qSqy8wGDJr5MbcxuhENYGWJRLE4P4OHqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/util": "^4.8.0", + "async-retry": "^1.3.3", + "axios": "^1.15.0", + "opossum": "^10.0.0" + } + }, + "node_modules/@sap-cloud-sdk/util": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.8.0.tgz", + "integrity": "sha512-wLWxgxYwAL1N5dh+m8XGZTZ2vzooDHXKXrkay3rBpYSFHhZjsD2V37ezbAhbBKlFIELZA03C+Eb9o4YgHpvTKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.15.0", + "logform": "^2.7.0", + "voca": "^1.4.1", + "winston": "^3.19.0", + "winston-transport": "^4.9.0" + } + }, "node_modules/@sap/cds": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.0.3.tgz", - "integrity": "sha512-S9q8vcJXzIsO4KC49sb9JLIhY/k0MJTZcgEOzmhvoBW/lWLLR79Oci0xmyQqSxJwwzjavR9mW8I4wbMJQQMVAA==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.0.5.tgz", + "integrity": "sha512-H5vTMVsznF4q24OVYkiWcReQezLOzKlVC3LBiHlrkg1D9x24V/OGWKCrCG+yd77hyIsrA2l2yxWE3RU4CElgmg==", "license": "SEE LICENSE IN LICENSE", "peer": true, "dependencies": { @@ -2435,9 +2470,9 @@ } }, "node_modules/@sap/cds-compiler": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.0.1.tgz", - "integrity": "sha512-Qwk9jitwSSwPB9FTB/Q6gYPxFsJxswhfsO9Ux77HTxjIhse8O/Hlq2XzkpVSTyaAVr8Lo7wLfpytHwKQScZTwQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.0.3.tgz", + "integrity": "sha512-scgBPK0TcobT0tXIQOEBTeVuGaxWthKeIQjhBoeLWcQXwVC3s61U3GFdbW56h01pXFwwUUSsxjzZN7all5lRyg==", "license": "SEE LICENSE IN LICENSE", "peer": true, "bin": { @@ -2546,6 +2581,20 @@ "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, + "node_modules/@sap/xssec": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.3.tgz", + "integrity": "sha512-op5wFTpJGJFdwFcEFRDX30yKbB2Kknk+LJqXDcrHyPNLqvSqerTWXFdFnylhiIhoAdGFSccC6NimUhqFxIrUsA==", + "dev": true, + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.52", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", @@ -2573,6 +2622,17 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -2678,9 +2738,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { @@ -2694,6 +2754,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3076,9 +3143,10 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3087,15 +3155,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3153,13 +3212,16 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -3192,6 +3254,19 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3202,6 +3277,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3219,6 +3304,16 @@ "dev": true, "license": "MIT" }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3227,14 +3322,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -3401,16 +3496,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -3470,6 +3565,13 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3538,9 +3640,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001807", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz", - "integrity": "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -3658,20 +3760,83 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=8" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { @@ -3686,6 +3851,20 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3706,6 +3885,52 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3912,6 +4137,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3920,9 +4155,9 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.402", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", - "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "version": "1.5.400", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", + "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", "dev": true, "license": "ISC" }, @@ -3940,9 +4175,16 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true, "license": "MIT" }, @@ -3984,6 +4226,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -4043,9 +4291,9 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "devOptional": true, "license": "MIT", "workspaces": [ @@ -4055,7 +4303,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -4079,7 +4327,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4365,6 +4613,13 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4445,12 +4700,19 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "devOptional": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -4878,9 +5140,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "peer": true, "dependencies": { @@ -4905,14 +5167,13 @@ } }, "node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -5412,19 +5673,6 @@ "fsevents": "^2.3.3" } }, - "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-leak-detector": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", @@ -5477,19 +5725,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-mock": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", @@ -5699,19 +5934,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-validate": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", @@ -5796,6 +6018,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jks-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.7.tgz", + "integrity": "sha512-BeiDRKsAi1NwEwgx2JB/9/0tar5BNGIv+foGm1G5GgiyR35s/iUnfd/BWqYd16mLDD8qTaAVBrIcOOuVqXJZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-forge": "^1.4.0", + "node-int64": "^0.4.0", + "node-rsa": "^1.1.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5871,6 +6105,75 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5881,6 +6184,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5935,6 +6245,73 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -6001,13 +6378,17 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", "peer": true, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/merge-descriptors": { @@ -6044,13 +6425,26 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, - "engines": { + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { "node": ">= 0.6" } }, @@ -6082,13 +6476,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -6182,6 +6576,16 @@ "node": ">= 8.0.0" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6190,15 +6594,25 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6257,6 +6671,16 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -6273,6 +6697,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opossum": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-10.0.0.tgz", + "integrity": "sha512-sghtqL8Usj+et06Zui0nyn0R6FFsl7cyuoU+d7MctYU0nbS7Htzjleh+tWohFqk1Rp1srrFwbFa8Vxd0O64w6A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^26 || ^24 || ^22" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6442,13 +6876,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -6573,9 +7007,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -6710,6 +7144,21 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6756,6 +7205,16 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -6773,12 +7232,42 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -6994,6 +7483,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7027,6 +7526,16 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7041,21 +7550,47 @@ "node": ">=10" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -7072,7 +7607,24 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -7085,6 +7637,22 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -7099,6 +7667,16 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -7162,9 +7740,9 @@ } }, "node_modules/systeminformation": { - "version": "5.31.11", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.11.tgz", - "integrity": "sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==", + "version": "5.33.1", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", + "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==", "dev": true, "license": "MIT", "os": [ @@ -7181,7 +7759,7 @@ "systeminformation": "lib/cli.js" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "funding": { "type": "Buy me a coffee", @@ -7256,6 +7834,13 @@ "node": "*" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7286,6 +7871,16 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7473,6 +8068,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7513,6 +8115,13 @@ "node": ">=0.6.0" } }, + "node_modules/voca": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz", + "integrity": "sha512-NJC/BzESaHT1p4B5k4JykxedeltmNbau4cummStd4RjFojgq/kLew5TzYge9N2geeWyI2w8T30wUET5v+F7ZHA==", + "dev": true, + "license": "MIT" + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7539,6 +8148,44 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7557,18 +8204,18 @@ "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7593,6 +8240,64 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7675,6 +8380,51 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a6291419..d9c2d5a2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@opentelemetry/exporter-trace-otlp-proto": "^0.219", "@opentelemetry/instrumentation-host-metrics": "^0.2.0", "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 3b15627c..ac52e303 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -1,4 +1,4 @@ -// REVISIT: use native fetch in cds oq +// Use native fetch in CDS OQ so @opentelemetry/instrumentation-undici can see outbound calls process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js new file mode 100644 index 00000000..976ee348 --- /dev/null +++ b/test/tracing-remote-cloudsdk.test.js @@ -0,0 +1,61 @@ +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const http = require('http') + +// Cloud SDK path: with @sap-cloud-sdk/http-client installed (as in the bookshop) and +// cds.env.remote.native_fetch NOT set, CAP routes outbound remote calls through +// getCloudSdk().executeHttpRequestWithOrigin(...). lib/tracing/cloud_sdk.js wraps that +// export so the outbound call produces a @cap-js/telemetry CLIENT span carrying +// the sap.btp.destination attribute. +describe('tracing remote via cloud sdk', () => { + // cloud-sdk resilience module resolution has issues on cds 8 + if (Number(cds.version.split('.')[0]) < 9) return + + const log = jest.spyOn(console, 'dir') + beforeEach(log.mockClear) + + const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') + + let server, port + + beforeAll(done => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + done() + }) + }) + + afterAll(() => new Promise(resolve => server.close(resolve))) + + test('outbound call is traced by the cloud_sdk wrapper with sap.btp.destination', async () => { + // a named destination object -> destination.name flows into the CLIENT span attribute + cds.env.requires.TestRemote = { + kind: 'odata', + credentials: { destination: { name: 'my-destination', url: `http://localhost:${port}` } } + } + const remote = await cds.connect.to('TestRemote') + + // no mock handler - let it make the actual HTTP call via the cloud sdk + await remote.send({ method: 'GET', path: '/test' }) + + // the cloud sdk path must not go through native fetch / undici + expect(cds.env.remote?.native_fetch).not.to.equal(true) + + // the outbound span comes from our tracer (not from undici) ... + const clientSpan = getCapSpans().find(s => s.attributes?.['code.function.name'] === 'executeHttpRequestWithOrigin') + expect(clientSpan, 'cloud_sdk wrapper did not produce a CLIENT span').to.exist + // ... is a CLIENT span (kind 2) ... + expect(clientSpan.kind).to.equal(2) + // ... and carries the destination name + expect(clientSpan.attributes['sap.btp.destination']).to.equal('my-destination') + + // no undici span for this call (cloud sdk path is used, not native fetch) + const undiciSpans = getSpans().filter(s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici') + expect(undiciSpans.length).to.equal(0) + }) +}) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js new file mode 100644 index 00000000..5a6947cc --- /dev/null +++ b/test/tracing-remote-native.test.js @@ -0,0 +1,57 @@ +// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk). +// This must be set before @sap/cds is loaded, so it lives at the very top of the file. +process.env.cds_remote_native__fetch = 'true' + +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const http = require('http') + +// Native fetch path: when cds.env.remote.native_fetch === true (or no cloud sdk is +// installed), CAP routes outbound remote calls through native fetch, which is +// instrumented by @opentelemetry/instrumentation-undici. The outbound span therefore +// comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and +// NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. +describe('tracing remote via native fetch', () => { + // cloud-sdk resilience module resolution has issues on cds 8 + if (Number(cds.version.split('.')[0]) < 9) return + + const log = jest.spyOn(console, 'dir') + beforeEach(log.mockClear) + + const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + + let server, port + + beforeAll(done => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + done() + }) + }) + + afterAll(() => new Promise(resolve => server.close(resolve))) + + test('outbound call is traced by @opentelemetry/instrumentation-undici', async () => { + expect(cds.env.remote?.native_fetch).to.equal(true) + + cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } + const remote = await cds.connect.to('TestRemote') + + // no mock handler - let it make the actual HTTP call via native fetch + await remote.send({ method: 'GET', path: '/test' }) + + const undiciSpan = getSpans().find( + s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici' + ) + expect(undiciSpan, 'no span from @opentelemetry/instrumentation-undici').to.exist + expect(undiciSpan.attributes['http.request.method']).to.equal('GET') + expect(undiciSpan.attributes['http.response.status_code']).to.equal(200) + expect(undiciSpan.attributes['url.full']).to.equal(`http://localhost:${port}/test`) + expect(undiciSpan.attributes['server.address']).to.equal('localhost') + expect(undiciSpan.attributes['server.port']).to.equal(port) + }) +}) From b439634e103ea158ffb136f6b619a90a0b491869 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:01:55 +0200 Subject: [PATCH 04/17] ci: target develop for dependabot updates (#470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `target-branch: 'develop'` to both dependabot update entries (npm + github-actions) so dependency-bump PRs open against `develop` instead of the default branch (`main`). They then promote to `main` via the reviewed `develop → main` PR, like every other change. --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dab5e766..612ef038 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,7 @@ version: 2 updates: - package-ecosystem: 'npm' # See documentation for possible values directory: '/' # Location of package manifests + target-branch: 'develop' versioning-strategy: increase-if-necessary schedule: interval: 'weekly' @@ -26,6 +27,7 @@ updates: - package-ecosystem: 'github-actions' directory: '/' + target-branch: 'develop' schedule: interval: 'weekly' cooldown: From 7790a99603cc960c909b5cfb1945f7d9da3e492d Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:38:52 +0200 Subject: [PATCH 05/17] feat: trace queue worker transactions + structured span test infrastructure (#465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Added Wraps `cds.Service.prototype.tx()` so the queue worker's two-transaction structure (tx1: SELECT+UPDATE lock, tx2: handle+DELETE dispatch) appears as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of each top-level CAP call becoming an orphan root. Guarded so `$batch` sub-requests (active `EventContext`) are unaffected; file-based messaging consumer delivery (bare `{}` context) still gets a root span. ## Test infrastructure Replaces fragile `cds.test.log()` regex assertions with a structured in-memory span exporter (`MyInMemorySpanExporter`) and `groupedByTrace()` / `rootSpans()` helpers. Rewrites the existing tracing suites and adds coverage for scheduled tasks, outboxed batch fan-out, and inbox/outbox messaging combinations. ## SQLite note Queue-worker suites skip on sqlite (published `@sap/cds` uses a `setTimeout` bypass, not `cds.spawn`) — verified on HANA in CI. Follow-up #467 removes the skips once the cds queue-spawn fix ships. Changelog updated. Targets `develop`. --- CHANGELOG.md | 2 + lib/tracing/cds.js | 19 ++ test/bookshop/.cdsrc.json | 21 ++ test/bookshop/lib/MyInMemorySpanExporter.js | 59 +++++ test/bookshop/srv/admin-service.cds | 3 + test/bookshop/srv/admin-service.js | 24 ++ test/console-span-exporter.test.js | 243 ++++++++++++++++++ test/tracing-attributes.test.js | 79 +++--- test/tracing-messaging-inboxed.test.js | 66 +++++ ...racing-messaging-persistent-outbox.test.js | 104 +++++++- test/tracing-messaging-without-outbox.test.js | 39 ++- test/tracing-messaging.js | 22 +- test/tracing-mt.test.js | 20 +- test/tracing-outboxed-batch.test.js | 72 ++++++ test/tracing-scheduled.test.js | 73 ++++++ test/tracing.test.js | 71 +++-- 16 files changed, 818 insertions(+), 99 deletions(-) create mode 100644 test/bookshop/lib/MyInMemorySpanExporter.js create mode 100644 test/console-span-exporter.test.js create mode 100644 test/tracing-messaging-inboxed.test.js create mode 100644 test/tracing-outboxed-batch.test.js create mode 100644 test/tracing-scheduled.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3995a012..39154001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added +- Queue worker transactions are traced as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans + ### Changed ### Fixed diff --git a/lib/tracing/cds.js b/lib/tracing/cds.js index c6766273..07445ef0 100644 --- a/lib/tracing/cds.js +++ b/lib/tracing/cds.js @@ -46,6 +46,25 @@ module.exports = () => { } }) + // Wrap `srv.tx(fn)` so the queue worker's two transactions (SELECT+UPDATE lock tx, + // then handle+DELETE dispatch tx) appear as child spans of the `cds.spawn - run task` + // root instead of each top-level CAP call inside them becoming an orphan root. + // Only wraps when there is no current `srv.context` (i.e. not already inside a request). + const _tx_proto = cds.Service.prototype.tx + cds.Service.prototype.tx = wrap(_tx_proto, { + wrapper: function tx() { + const fnIdx = typeof arguments[0] === 'function' ? 0 : typeof arguments[1] === 'function' ? 1 : -1 + if (fnIdx < 0) return _tx_proto.apply(this, arguments) + // Skip if this service is already handling a request (has an active EventContext), + // or if this is a nested .tx() call (cds.Service.tx is a no-op when already in tx). + // Do NOT skip for a bare {} context set by processInboundMsg — that's the entry point + // for file-based messaging consumer delivery and should get a root span. + if (this.context instanceof cds.EventContext) return _tx_proto.apply(this, arguments) + const name = `${this.name || 'cds'} - tx` + return trace(name, _tx_proto, this, arguments, {}) + } + }) + const { spawn: _spawn } = cds cds.spawn = wrap(_spawn, { wrapper: function spawn() { diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 1dc257b9..6571cbe7 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -82,6 +82,18 @@ } } }, + "[tracing-in-memory]": { + "requires": { + "telemetry": { + "tracing": { + "exporter": { + "module": "./lib/MyInMemorySpanExporter.js", + "class": "MyInMemorySpanExporter" + } + } + } + } + }, "[persistent-outbox]": { "requires": { "messaging": { @@ -90,6 +102,15 @@ } } }, + "[inboxed]": { + "requires": { + "messaging": { + "kind": "file-based-messaging", + "file": "../inboxed", + "inboxed": true + } + } + }, "[without-outbox]": { "requires": { "messaging": { diff --git a/test/bookshop/lib/MyInMemorySpanExporter.js b/test/bookshop/lib/MyInMemorySpanExporter.js new file mode 100644 index 00000000..9c7e4bdc --- /dev/null +++ b/test/bookshop/lib/MyInMemorySpanExporter.js @@ -0,0 +1,59 @@ +// In-memory span exporter for tests. Spans are accumulated in a module-level array that +// tests can import directly via `require('./lib/MyInMemorySpanExporter').captured`. +// Wired into the tracer provider via .cdsrc.json profile config (no provider-poking from tests). + +const { ExportResultCode } = require('@opentelemetry/core') + +const captured = [] + +class MyInMemorySpanExporter { + export(spans, resultCallback) { + captured.push(...spans) + resultCallback({ code: ExportResultCode.SUCCESS }) + } + + shutdown() { + return Promise.resolve() + } + + forceFlush() { + return Promise.resolve() + } +} + +// Returns the captured spans grouped by traceId, each group is a hierarchy: +// { traceId, root, all, byParent } +// `root` is the span with no parent inside the group (the visible root for the exporter's +// "elapsed times:" primer logic — i.e. spans whose parentSpanId is not present in this group). +function groupedByTrace() { + const byTrace = new Map() + for (const s of captured) { + const tid = s.spanContext().traceId + if (!byTrace.has(tid)) byTrace.set(tid, []) + byTrace.get(tid).push(s) + } + + return [...byTrace.entries()].map(([traceId, all]) => { + const ids = new Set(all.map(s => s.spanContext().spanId)) + const roots = all.filter(s => !s.parentSpanContext?.spanId || !ids.has(s.parentSpanContext.spanId)) + const byParent = new Map() + for (const s of all) { + const pid = s.parentSpanContext?.spanId + if (!byParent.has(pid)) byParent.set(pid, []) + byParent.get(pid).push(s) + } + return { traceId, root: roots[0], roots, all, byParent } + }) +} + +// Returns just the visible "root" spans across all captured traces. These correspond 1:1 to +// "elapsed times:" primers our ConsoleSpanExporter would emit for the same data. +function rootSpans() { + return groupedByTrace().flatMap(g => g.roots) +} + +function reset() { + captured.length = 0 +} + +module.exports = { MyInMemorySpanExporter, captured, groupedByTrace, rootSpans, reset } diff --git a/test/bookshop/srv/admin-service.cds b/test/bookshop/srv/admin-service.cds index 4ab230d8..3d8838ec 100644 --- a/test/bookshop/srv/admin-service.cds +++ b/test/bookshop/srv/admin-service.cds @@ -7,6 +7,9 @@ service AdminService @(requires: 'admin') { action test_spawn(); action test_emit(); + action test_outboxed_send(); + action test_outboxed_send_batch(); + action test_scheduled(); event foo { bar : String; diff --git a/test/bookshop/srv/admin-service.js b/test/bookshop/srv/admin-service.js index fb1cdf6c..9cb186fe 100644 --- a/test/bookshop/srv/admin-service.js +++ b/test/bookshop/srv/admin-service.js @@ -32,6 +32,30 @@ module.exports = class AdminService extends cds.ApplicationService { await messaging.emit('foo', { bar: 'baz' }) }) + // test_outboxed_send: writes a task to the persistent outbox addressed to ExternalServiceOne, + // whose handler the test installs. Exercises the queue-worker path (scan, lock, dispatch). + this.on('test_outboxed_send', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + await cds.queued(externalOne).send('call', {}) + }) + + // test_outboxed_send_batch: writes multiple tasks to the persistent outbox to exercise chunkSize > 1 fan-out. + this.on('test_outboxed_send_batch', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + const queued = cds.queued(externalOne) + await Promise.all([ + queued.send('call', {}), + queued.send('call', {}), + queued.send('call', {}) + ]) + }) + + // test_scheduled: schedules a one-shot task to fire after a short delay. + this.on('test_scheduled', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + await cds.queued(externalOne).schedule('call', {}).after(10) + }) + return super.init() } } diff --git a/test/console-span-exporter.test.js b/test/console-span-exporter.test.js new file mode 100644 index 00000000..4c83e4e8 --- /dev/null +++ b/test/console-span-exporter.test.js @@ -0,0 +1,243 @@ +// Unit tests for ConsoleSpanExporter — verifies the user-friendly hierarchy formatting +// (the "elapsed times:" primer + indented child lines) by feeding the exporter crafted +// ReadableSpan-shaped fixtures and inspecting the formatted string passed to LOG.info. +// +// This is a pure unit test: no cds.test server, no real OTel SDK, no console spying. + +const cds = require('@sap/cds') + +// Hook LOG.info BEFORE requiring the exporter so the exporter's module-level +// `cds.log('telemetry')` resolves to a logger whose .info we control. +const infoCalls = [] +const telemetryLog = cds.log('telemetry') +const originalInfo = telemetryLog.info +telemetryLog.info = (...args) => infoCalls.push(args) + +const ConsoleSpanExporter = require('../lib/exporter/ConsoleSpanExporter') + +afterAll(() => { + telemetryLog.info = originalInfo +}) + +beforeEach(() => { + infoCalls.length = 0 +}) + +// --- helpers --------------------------------------------------------------- + +// Builds a minimal ReadableSpan-shaped object. Times are in OTel HrTime = [seconds, nanos]. +function span({ name, traceId, spanId, parentSpanId, startMs = 0, durationMs = 0, attributes = {} }) { + const startHr = msToHr(startMs) + const durationHr = msToHr(durationMs) + const endHr = msToHr(startMs + durationMs) + return { + name, + kind: 0, + spanContext: () => ({ traceId, spanId }), + parentSpanContext: parentSpanId ? { traceId, spanId: parentSpanId } : undefined, + startTime: startHr, + endTime: endHr, + duration: durationHr, + status: { code: 0 }, + attributes, + links: [], + events: [], + ended: true, + resource: { attributes: {} }, + instrumentationScope: { name: 'test' }, + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0 + } +} + +function msToHr(ms) { + const seconds = Math.floor(ms / 1000) + const nanos = Math.round((ms - seconds * 1000) * 1e6) + return [seconds, nanos] +} + +// Drives the exporter and returns the lines logged across all root primers. +function exportAndCapture(spans) { + const exporter = new ConsoleSpanExporter() + let result + exporter.export(spans, r => (result = r)) + expect(result).to.deep.equal({ code: 0 /* ExportResultCode.SUCCESS */ }) + return infoCalls.map(args => args[0]) +} + +// --- assertions ------------------------------------------------------------ + +const { expect } = require('@cap-js/cds-test') + +describe('ConsoleSpanExporter', () => { + describe('hierarchy formatting', () => { + it('emits a single "elapsed times:" primer per root and nests children by depth', () => { + // Tree shape: + // root (0 → 10 ms) + // childA (1 → 4 ms) + // grandchild (2 → 3 ms) + // childB (5 → 9 ms) + const TRACE = 'a'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) + const childA = span({ name: 'childA', traceId: TRACE, spanId: 'cA', parentSpanId: 'r0', startMs: 1, durationMs: 3 }) + const grand = span({ name: 'grandchild', traceId: TRACE, spanId: 'g0', parentSpanId: 'cA', startMs: 2, durationMs: 1 }) + const childB = span({ name: 'childB', traceId: TRACE, spanId: 'cB', parentSpanId: 'r0', startMs: 5, durationMs: 4 }) + + // Order matters: children must arrive BEFORE the root for the exporter's + // temporaryStorage flush logic to merge them under the same primer. + const [primer] = exportAndCapture([childA, grand, childB, root]) + + // Single primer + expect(infoCalls.length).to.equal(1) + expect(primer).to.match(/^elapsed times:/) + + // Root line: 0.00 → 10.00 = 10.00 ms root (no indent on the root data line) + expect(primer).to.match(/\n +0\.00 → +10\.00 = +10\.00 ms {2}root/) + + // First-level children indented by 2 spaces beyond root + expect(primer).to.match(/\n.+ ms {4}childA/) + expect(primer).to.match(/\n.+ ms {4}childB/) + + // Grandchild indented by 4 spaces beyond root + expect(primer).to.match(/\n.+ ms {6}grandchild/) + + // Ordering: childA appears before grandchild appears before childB + expect(primer.indexOf('childA')).to.be.lessThan(primer.indexOf('grandchild')) + expect(primer.indexOf('grandchild')).to.be.lessThan(primer.indexOf('childB')) + }) + + it('relativizes child start/end to the root start time', () => { + // Root starts at 100 ms wallclock; child at 105 ms. Child should display as 5.00 → ... + const TRACE = 'b'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 100, durationMs: 20 }) + const child = span({ name: 'child', traceId: TRACE, spanId: 'c0', parentSpanId: 'r0', startMs: 105, durationMs: 10 }) + + const [primer] = exportAndCapture([child, root]) + + expect(primer).to.match(/0\.00 → +20\.00 = +20\.00 ms {2}root/) + expect(primer).to.match(/5\.00 → +15\.00 = +10\.00 ms {4}child/) + }) + + it('sorts sibling spans by start time, ties broken by later end-time first', () => { + const TRACE = 'c'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 50 }) + const late = span({ name: 'late', traceId: TRACE, spanId: 's3', parentSpanId: 'r0', startMs: 10, durationMs: 1 }) + const earlyLong = span({ name: 'earlyLong', traceId: TRACE, spanId: 's1', parentSpanId: 'r0', startMs: 0, durationMs: 30 }) + const earlyShort = span({ name: 'earlyShort', traceId: TRACE, spanId: 's2', parentSpanId: 'r0', startMs: 0, durationMs: 5 }) + + const [primer] = exportAndCapture([late, earlyShort, earlyLong, root]) + + // Equal start time → longer span first; otherwise by start time ascending + const order = ['earlyLong', 'earlyShort', 'late'].map(n => primer.indexOf(n)) + expect(order[0]).to.be.lessThan(order[1]) + expect(order[1]).to.be.lessThan(order[2]) + }) + + it('emits a separate primer per trace (multi-root)', () => { + const T1 = 'd'.repeat(32), + T2 = 'e'.repeat(32) + const r1 = span({ name: 'root1', traceId: T1, spanId: 'r1', startMs: 0, durationMs: 5 }) + const c1 = span({ name: 'c1', traceId: T1, spanId: 'c1', parentSpanId: 'r1', startMs: 1, durationMs: 2 }) + const r2 = span({ name: 'root2', traceId: T2, spanId: 'r2', startMs: 0, durationMs: 7 }) + const c2 = span({ name: 'c2', traceId: T2, spanId: 'c2', parentSpanId: 'r2', startMs: 1, durationMs: 3 }) + + exportAndCapture([c1, c2, r1, r2]) + + expect(infoCalls.length).to.equal(2) + const all = infoCalls.map(c => c[0]) + expect(all[0]).to.include('root1').and.to.include('c1').and.not.to.include('root2') + expect(all[1]).to.include('root2').and.to.include('c2').and.not.to.include('root1') + }) + + it('skips short "METHOD /word" spans (e.g. unadjusted http instrumentation roots)', () => { + const TRACE = 'f'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) + // The skip regex is /^[A-Z]+ \/\${0,1}\w+$/ — single path segment, no slashes after the first. + const noisy = span({ name: 'GET /catalog', traceId: TRACE, spanId: 'h0', parentSpanId: 'r0', startMs: 1, durationMs: 5 }) + + const [primer] = exportAndCapture([noisy, root]) + + expect(primer).to.include('root') + expect(primer).not.to.include('GET /catalog') + }) + + it('handles deep nesting with increasing indentation', () => { + const TRACE = '1'.repeat(32) + const root = span({ name: 'L0', traceId: TRACE, spanId: 'L0', startMs: 0, durationMs: 10 }) + const l1 = span({ name: 'L1', traceId: TRACE, spanId: 'L1', parentSpanId: 'L0', startMs: 1, durationMs: 8 }) + const l2 = span({ name: 'L2', traceId: TRACE, spanId: 'L2', parentSpanId: 'L1', startMs: 2, durationMs: 6 }) + const l3 = span({ name: 'L3', traceId: TRACE, spanId: 'L3', parentSpanId: 'L2', startMs: 3, durationMs: 4 }) + + const [primer] = exportAndCapture([l1, l2, l3, root]) + + // Each deeper level adds 2 spaces of indentation + const indents = ['L0', 'L1', 'L2', 'L3'].map(n => { + const m = primer.match(new RegExp(`\\n( +)\\d.*ms( +)${n}(?!\\d)`)) + return m ? m[2].length - 1 : null // exclude the single space separator after "ms " + }) + // L0: 1 leading space before the name; each child adds 2. So we expect 1, 3, 5, 7. + expect(indents).to.deep.equal([1, 3, 5, 7]) + }) + }) + + describe('time formatting', () => { + it('formats sub-millisecond durations with two decimals', () => { + const TRACE = '2'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 0.5 }) + const [primer] = exportAndCapture([root]) + expect(primer).to.match(/0\.00 → +0\.50 = +0\.50 ms/) + }) + + it('right-aligns integer portion to 3 chars', () => { + const TRACE = '3'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 123 }) + const [primer] = exportAndCapture([root]) + // "123.00" → matches as-is, fits in the 3-char integer slot + expect(primer).to.match(/0\.00 → +123\.00 = +123\.00 ms/) + }) + }) + + describe('span name handling', () => { + it('truncates names longer than 80 chars with an ellipsis', () => { + const TRACE = '4'.repeat(32) + const longName = 'X'.repeat(100) + const root = span({ name: longName, traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 1 }) + + const [primer] = exportAndCapture([root]) + + expect(primer).to.include('X'.repeat(79) + '…') + expect(primer).not.to.include('X'.repeat(80)) + }) + }) + + describe('robustness', () => { + it('does not throw when a child arrives without its parent (orphan trace)', () => { + // No root provided for this trace — the exporter should buffer the child and not flush. + const TRACE = '5'.repeat(32) + const orphan = span({ name: 'orphan', traceId: TRACE, spanId: 'o0', parentSpanId: 'r-missing', startMs: 0, durationMs: 1 }) + + expect(() => exportAndCapture([orphan])).not.to.throw() + expect(infoCalls.length).to.equal(0) + }) + + it('treats any span without a parent as a root and emits a primer', () => { + const TRACE = '6'.repeat(32) + const lonely = span({ name: 'lonely', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 2 }) + + const [primer] = exportAndCapture([lonely]) + + expect(primer).to.match(/^elapsed times:/) + expect(primer).to.include('lonely') + }) + + it('shutdown flushes pending buffered children without throwing', () => { + const exporter = new ConsoleSpanExporter() + // No-op: just verify the shutdown contract. + return exporter.shutdown().then(() => { + // No exception, no logged primers (nothing was buffered). + expect(infoCalls.length).to.equal(0) + }) + }) + }) +}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index ac52e303..03204b13 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -2,15 +2,25 @@ process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') -describe('tracing attributes', () => { - beforeEach(data.reset) +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy, no provider-poking. +const { captured } = require('./bookshop/lib/MyInMemorySpanExporter') + +beforeEach(async () => { + // data.reset is itself heavily traced (it runs DELETEs + INSERTs for the seed data) — + // run it first, THEN clear the buffer so the test only sees its own spans. + await data.reset() + captured.length = 0 +}) - const log = jest.spyOn(console, 'dir') - beforeEach(log.mockClear) +// Returns all finished spans, optionally filtered by a predicate. +const spans = filter => (filter ? captured.filter(filter) : captured.slice()) +describe('tracing attributes', () => { describe('remote', () => { let server, port @@ -30,6 +40,9 @@ describe('tracing attributes', () => { }) test('HTTP client attributes are set on remote service span', async () => { + // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment + if (Number(cds.version.split('.')[0]) < 9) return + // configure destination URL directly on credentials cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } const remote = await cds.connect.to('TestRemote') @@ -37,54 +50,62 @@ describe('tracing attributes', () => { // no mock handler - let it make the actual HTTP call await remote.send({ method: 'GET', path: '/test' }) - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/"http\.request\.method":"GET"/) - expect(output).to.match(/"http\.response\.status_code":200/) - expect(output).to.match(new RegExp(`"url\\.full":"http://localhost:${port}/test"`)) - expect(output).to.match(/"server\.address":"localhost"/) - expect(output).to.match(new RegExp(`"server\\.port":${port}`)) + // Find the HTTP client span (instrumented by OTel's http instrumentation) + const httpSpan = spans(s => s.attributes['http.request.method'] === 'GET' && s.attributes['url.full']) + expect(httpSpan.length).to.be.gte(1, 'expected an HTTP client span') + const attrs = httpSpan[0].attributes + expect(attrs).to.include({ + 'http.request.method': 'GET', + 'http.response.status_code': 200, + 'url.full': `http://localhost:${port}/test`, + 'server.address': 'localhost', + 'server.port': port + }) }) }) describe('db', () => { const _db_spans = require('./_db_spans') - // prettier-ignore - const _get_db_spans = o => JSON.parse(o).map(o => o[0]).filter(s => !s.name.startsWith('db')) - const _match_db_spans = (output, kind) => { - const db_spans = _get_db_spans(output) - for (const each of _db_spans[kind]) expect(db_spans).to.containSubset([each]) + + // Filter out the high-level "db - …" CAP wrapper spans, keep only the @cap-js/ ones + // that carry the actual DB attributes. + const dbSpans = () => spans(s => !s.name.startsWith('db')) + + const _match_db_spans = kind => { + const got = dbSpans().map(s => ({ name: s.name, attributes: { ...s.attributes } })) + for (const each of _db_spans[kind]) expect(got).to.containSubset([each]) } test('SELECT', async () => { await SELECT.from('sap.capire.bookshop.Books').where('title !=', 'DUMMY') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":5/) - _match_db_spans(output, 'SELECT') + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(5) + _match_db_spans('SELECT') }) test('INSERT', async () => { await INSERT.into('sap.capire.bookshop.Books').entries([{ ID: 1 }, { ID: 2 }]) - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":2/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(2) // TODO - // _match_db_spans(output, 'INSERT') + // _match_db_spans('INSERT') }) test('UPDATE', async () => { await UPDATE('sap.capire.bookshop.Books').set({ stock: 42 }).where('ID > 250') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":3/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(3) // TODO - // _match_db_spans(output, 'UPDATE') + // _match_db_spans('UPDATE') }) test('DELETE', async () => { await DELETE.from('sap.capire.bookshop.Books') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":0/) //> texts - expect(output).to.match(/db\.client\.response.returned_rows":5/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(0) // texts + expect(rowCounts).to.include(5) // TODO - // _match_db_spans(output, 'DELETE') + // _match_db_spans('DELETE') }) }) }) diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js new file mode 100644 index 00000000..a5ec21e3 --- /dev/null +++ b/test/tracing-messaging-inboxed.test.js @@ -0,0 +1,66 @@ +const CASE = 'inboxed' + +// `inboxed: true` combined with the default outboxed messaging behavior means TWO queue +// workers get involved per emit — one on the producer side (drains outbox to broker) and +// one on the consumer side (drains inbox to subscribers). Each worker runs two +// transactions (tx 1: lock; tx 2: handle + delete). +// +// Each worker iteration is wrapped by `cds.spawn`, so both txs collapse under a single +// `cds.spawn - run task` root. 4 meaningful roots: +// +// 1. AdminService - tx (producer: handle test_emit, UPSERT outbox) +// 2. cds.spawn - run task (outbox worker: dispatches to file) +// ├─ db - tx (tx 1: lock) +// └─ messaging - tx (tx 2: handle foo — writes to file — + DELETE) +// 3. messaging - tx (file-based CONSUMER: writes inbox row) +// └─ ...enqueue into inbox... +// 4. cds.spawn - run task (inbox worker: runs subscriber) +// ├─ db - tx (tx 1: lock) +// └─ messaging - tx (tx 2: handle foo — SELECT Books — + DELETE) +// +// Tolerated: allow one extra root for the scheduling-service bookkeeping startup scan. + +// REVISIT: profile config wins for kind/file, but explicit env override sidesteps it. +process.env.cds_requires_messaging = JSON.stringify({ + kind: 'file-based-messaging', + file: `../${CASE}`, + inboxed: true +}) + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + + // The inbox worker must have run the application handler (SELECT Books). + const allSpans = groupedByTrace.flatMap(g => g.all) + expect(allSpans.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/))).to.be.true + expect(allSpans.some(s => s.name === 'db - DELETE cds.outbox.Messages')).to.be.true + + // Exactly two `cds.spawn - run task` roots (outbox worker + inbox worker). + const workerRoots = rootSpans.filter(s => s.name === 'cds.spawn - run task') + expect(workerRoots, 'expected two queue-worker spawn roots (outbox + inbox)').to.have.lengthOf(2) + + // One of the spawn roots (the inbox worker) ran the app handler. + const inboxWorker = groupedByTrace.find( + g => g.root.name === 'cds.spawn - run task' && g.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/)) + ) + expect(inboxWorker, 'expected an inbox-worker trace that ran the application handler').to.exist + + // 4 meaningful roots (+1 tolerated bookkeeping scan). + expect(rootSpans.length).to.be.gte(4) + expect(rootSpans.length).to.be.lte(5) +} + +const cds = require('@sap/cds') + +describe(`tracing messaging - ${CASE}`, () => { + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) +}) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 1d959595..82b89109 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -6,15 +6,101 @@ process.env.cds_requires_messaging = JSON.stringify({ file: `../${CASE}` }) -// REVISIT: check json exports -const CHECK = (log, expect) => { - // 3: outbox -> consumers get new root context - // REVISIT: for some reason, span "cds.spawn run task" has no parent when running in jest - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(4) //> actually 3 - expect(log.output.match(/cds.spawn - schedule task/g).length).to.equal(1) +// --- Span hierarchy for the persistent-outbox case --------------------------------------- +// +// With persistent outbox enabled, the queue worker runs two coherent transactions: +// - tx 1: read out of queue + set status='processing' (libx/queue/processing.js:189) +// - tx 2: handle the event + delete the row (libx/queue/processing.js:319) +// +// `@cap-js/telemetry` wraps `cds.tx(fn)` to emit a ` - tx` span per callback, so +// each of these transactions is captured as a root/child span. Both sqlite and HANA now +// produce the same unified shape: the worker uses `cds.spawn`, which the telemetry plugin +// wraps to emit a single `cds.spawn - run task` CONSUMER root that both worker tx spans +// nest under. +// +// Expected shape (3 meaningful roots, same for sqlite and HANA): +// +// 1. AdminService - tx (producer trace) +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task +// +// 2. cds.spawn - run task (queue worker root) +// ├─ db - tx (tx 1) +// │ ├─ db - READ cds.outbox.Messages +// │ └─ db - UPDATE cds.outbox.Messages +// └─ messaging - tx (tx 2) +// ├─ messaging - handle foo +// └─ db - DELETE cds.outbox.Messages +// +// 3. messaging - tx (file-based CONSUMER) +// └─ ...handler work (READ Books, READ Authors)... +// +// Plus the scheduling service may emit a bookkeeping `db - tx` (startup scan finding no +// tasks) — tolerated as a 4th root, not required. + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + + // Queue worker trace: rooted at `cds.spawn - run task`, containing both tx spans as children. + const workerTrace = groupedByTrace.find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + + // tx 1: db - tx with READ + UPDATE of the outbox + const workerDbTx = workerTrace.all.find(s => s.name === 'db - tx') + expect(workerDbTx, 'expected a db - tx child in the worker trace (tx 1)').to.exist + expect(workerTrace.all.some(s => s.name === 'db - READ cds.outbox.Messages')).to.be.true + expect(workerTrace.all.some(s => s.name === 'db - UPDATE cds.outbox.Messages')).to.be.true + + // tx 2: messaging - tx with handle foo + DELETE of the outbox row + const workerMessagingTx = workerTrace.all.find(s => s.name === 'messaging - tx') + expect(workerMessagingTx, 'expected a messaging - tx child in the worker trace (tx 2)').to.exist + expect(workerTrace.all.some(s => s.name === 'messaging - handle foo')).to.be.true + expect(workerTrace.all.some(s => s.name === 'db - DELETE cds.outbox.Messages')).to.be.true + + // File-based CONSUMER trace (the file-messaging consumer, *not* the queue-worker path). + // Identified by containing the full `foo` handler work (SELECT Books + READ Authors). + const consumer = groupedByTrace.find( + g => + g !== producer && + g !== workerTrace && + g.root.name === 'messaging - tx' && + g.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/)) && + g.all.some(s => s.name === 'AdminService - READ AdminService.Authors') + ) + expect(consumer, 'expected a CONSUMER trace').to.exist + expect(consumer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true + expect(consumer.all.some(s => s.name === 'messaging - handle foo')).to.be.true + + // 3 meaningful roots; tolerate one extra for the scheduling-service bookkeeping scan + // (a `db - tx` root with just a READ, no UPDATE). + expect(rootSpans.length).to.be.gte(3) + expect(rootSpans.length).to.be.lte(4) + + // Sanity: every non-root span has a parent inside the captured set. + const allSpans = groupedByTrace.flatMap(g => g.all) + for (const s of allSpans) { + const pid = s.parentSpanContext?.spanId + if (!pid) continue + const parent = allSpans.find(p => p.spanContext().spanId === pid) + expect(parent, `expected parent span for ${s.name}`).to.exist + } } -// REVISIT: re-enable with switch to vitest -describe.skip(`tracing messaging - ${CASE}`, () => { - require('./tracing-messaging')(CASE, CHECK) +const cds = require('@sap/cds') + +describe(`tracing messaging - ${CASE}`, () => { + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) }) diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index f93664c1..81138218 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -7,10 +7,41 @@ process.env.cds_requires_messaging = JSON.stringify({ outboxed: false }) -// REVISIT: check json exports -const CHECK = (log, expect) => { - // 2: no outbox -> consumer gets new root context - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(2) +// Without outbox, file-based messaging writes directly to the file from the producer's +// transaction (no queue worker). The file watcher delivers asynchronously as a new +// SpanKind.CONSUMER root. +// +// Expected roots: +// 1. AdminService - tx (producer) +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo (writes to file, in-process) +// +// 2. messaging - tx (file-based CONSUMER) +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo +// └─ ...handler work... +// +// The scheduling service may also emit a bookkeeping scan trace (`db - tx → db - READ +// cds.outbox.Messages` finding nothing) — we allow it but don't require it. + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name.match(/messaging - emit outgoing/))).to.be.true + + // File-based CONSUMER trace + const consumer = groupedByTrace.find( + g => g !== producer && g.root.name === 'messaging - tx' && g.all.some(s => s.name === 'messaging - handle foo') + ) + expect(consumer, 'expected a CONSUMER trace').to.exist + expect(consumer.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/))).to.be.true + + // 2 meaningful roots; allow up to 3 to tolerate the scheduling service's bookkeeping scan. + expect(rootSpans.length).to.be.gte(2) + expect(rootSpans.length).to.be.lte(3) } describe(`tracing messaging - ${CASE}`, () => { diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 35508745..15202e34 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -1,7 +1,7 @@ -module.exports = (CASE, CHECK) => { +module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { const cds = require('@sap/cds') - const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', CASE) - const log = cds.test.log() + const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) + const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const wait = require('node:timers/promises').setTimeout @@ -21,16 +21,22 @@ module.exports = (CASE, CHECK) => { }) afterAll(async () => { - await wait(100) + // Wait long enough for any background queue-worker / scheduling-service timers to + // fire one last time before jest tears down the env. Without this, those timers can + // fire after teardown and crash with "cds.error.isSystemError is not a function" + // (cds module is reloaded between tests, but the timer references the old instance). + await wait(2000) rm() }) - beforeEach(log.clear) + beforeEach(() => { + reset() + }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(1000) - // execute case specific check - CHECK(log, expect) + await wait(waitMs) + // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } + CHECK({ expect, rootSpans: rootSpans(), groupedByTrace: groupedByTrace(), captured: [...captured], cds }) }) } diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index f6f3c36a..872a6dc2 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -1,7 +1,8 @@ const cds = require('@sap/cds') // prettier-ignore -const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname + '/bookshop', '--profile', 'multitenancy') -const log = cds.test.log() +const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname + '/bookshop', '--profile', 'multitenancy,tracing-in-memory') + +const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' @@ -17,22 +18,23 @@ describe('tracing with multitenancy', () => { await mts.subscribe(TENANT2) }) - beforeEach(log.clear) + beforeEach(reset) test('GET with user1 is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', user1) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry|tenant_1\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + // AdminService READ ran exactly once and was tagged with the right tenant. + const spans = captured.filter(s => s.name === 'AdminService - READ AdminService.Books') + expect(spans.length, 'expected exactly one AdminService READ span').to.equal(1) + expect(spans[0].attributes['sap.tenancy.tenant_id']).to.equal(TENANT1) }) test('GET with user2 is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', user2) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry|tenant_2\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + const spans = captured.filter(s => s.name === 'AdminService - READ AdminService.Books') + expect(spans.length, 'expected exactly one AdminService READ span').to.equal(1) + expect(spans[0].attributes['sap.tenancy.tenant_id']).to.equal(TENANT2) }) // --- TODO --- diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js new file mode 100644 index 00000000..640689ea --- /dev/null +++ b/test/tracing-outboxed-batch.test.js @@ -0,0 +1,72 @@ +// Tests that when the queue worker picks up multiple ready tasks in one iteration +// (chunkSize > 1), each is dispatched in its own tx span under the SAME worker root. +// This validates the parallel-fan-out shape described in the design notes. + +const cds = require('@sap/cds') +const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') +const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') +const { hrTimeToNanoseconds } = require('@opentelemetry/core') + +const wait = require('node:timers/promises').setTimeout + +describe('tracing for outboxed batch (chunk-size fan-out)', () => { + if (Number(cds.version.split('.')[0]) < 9) { + test.skip('skipping for cds < 9', () => {}) + return + } + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + + beforeAll(async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + externalOne.on('call', () => 'ok') + }) + + beforeEach(reset) + + test('three queued sends produce parallel dispatch spans under one worker root', async () => { + await POST('/odata/v4/admin/test_outboxed_send_batch', {}, { auth: { username: 'alice' } }) + await wait(2500) + + // Producer wrote three rows to the outbox. + const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') + expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) + + // Look for a queue worker root containing multiple dispatch tx spans. + const workerTrace = groupedByTrace().find(g => + g.root.name === 'cds.spawn - run task' && + g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + ) + expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist + + // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… + const lockTxs = workerTrace.all.filter(s => + s.name === 'db - tx' && + workerTrace.all.some(c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages') + ) + expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) + + // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. + const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') + expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) + for (const tx of dispatchTxs) { + const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) + expect(kids.some(k => k.name.match(/ExternalServiceOne - handle/)), 'dispatch tx should contain handle call').to.be.true + expect(kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), 'dispatch tx should contain DELETE').to.be.true + } + + // The dispatch txs should overlap in time (parallel), not be strictly sequential. + if (dispatchTxs.length >= 2) { + const sorted = [...dispatchTxs].sort( + (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) + ) + const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) + const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) + // Parallel: second starts before first ends (allow a tiny slack). + expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) + } + }) +}) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js new file mode 100644 index 00000000..a5928956 --- /dev/null +++ b/test/tracing-scheduled.test.js @@ -0,0 +1,73 @@ +// Tests tracing of scheduled tasks. +// +// `cds.queued(svc).schedule('event', ...).after(N)` writes a task row to the persistent +// outbox with a timestamp N ms in the future. The queue scheduler picks it up at that +// time and dispatches to the target service's handler. +// +// Expected meaningful roots (unified across sqlite and HANA): +// +// 1. AdminService - tx (producer trace) +// └─ AdminService - handle test_scheduled +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task +// +// 2. cds.spawn - run task (queue worker root) +// ├─ db - tx (tx 1: lock) +// └─ ExternalServiceOne - tx (tx 2: dispatch) +// +// Plus optionally one bookkeeping startup-scan trace (tolerated, not required). +// Total meaningful roots: between 2 and 3. + +const cds = require('@sap/cds') +const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') +const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') + +const wait = require('node:timers/promises').setTimeout + +describe('tracing for scheduled tasks', () => { + if (Number(cds.version.split('.')[0]) < 9) { + test.skip('skipping for cds < 9', () => {}) + return + } + // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite + // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite + // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds + // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + + beforeAll(async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + externalOne.on('call', () => 'ok') + }) + + beforeEach(reset) + + test('schedule .after() is fully traced through the queue worker', async () => { + await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) + // wait long enough for the scheduled task to fire (10ms after-delay + worker latency) + await wait(1500) + + // Producer trace: writes the task row inside the HTTP request tx. + const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + + // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. + const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true + expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true + + // The ExternalServiceOne handler was invoked. + expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true + + // Total meaningful roots: producer + worker (+ optional bookkeeping scan). + expect(rootSpans().length).to.be.gte(2) + expect(rootSpans().length).to.be.lte(3) + }) +}) diff --git a/test/tracing.test.js b/test/tracing.test.js index f3d0ce05..b1cd985a 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -4,22 +4,27 @@ process.env.cds_requires_telemetry_tracing_sampler = JSON.stringify({ }) const cds = require('@sap/cds') -const { expect, GET, POST } = cds.test(__dirname + '/bookshop') -const log = cds.test.log() +const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') + +// Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter +// (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no +// console spying, no string-regex matching of formatted output. +const { reset, rootSpans, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const wait = require('node:timers/promises').setTimeout describe('tracing', () => { const admin = { auth: { username: 'alice' } } - beforeEach(log.clear) + beforeEach(reset) test('GET is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + // The AdminService READ for Books was traced + expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true + // ...and at least one trace was rooted (i.e. our exporter would emit "elapsed times:") + expect(rootSpans().length).to.be.gte(1) }) // REVISIT: jest breaks otel's patching of incoming request handling -> no span for 'GET' -> behavior to test not reproducible @@ -27,17 +32,13 @@ describe('tracing', () => { const config = { ...admin, headers: { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' } } const { status } = await GET('/odata/v4/admin/Books', config) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true }) test('custom GET is traced', async () => { const { status } = await GET('/custom/Books', admin) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - READ sap.capire.bookshop.Books/) + expect(captured.some(s => s.name === 'db - READ sap.capire.bookshop.Books')).to.be.true }) test('NonRecordingSpans are handled correctly', async () => { @@ -45,20 +46,13 @@ describe('tracing', () => { expect(postStatus).to.equal(201) const { status: getStatus } = await GET('/odata/v4/admin/Authors?$select=ID', admin) expect(getStatus).to.equal(200) - // primitive check that console has no trace logs - expect(log.output).not.to.match(/telemetry/) + // The sampler in this test ignores /odata/v4/admin/Authors — no spans should be captured for it. + // (Other unrelated background work may still produce spans; assert only that none mention Authors.) + expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) }) // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible - xtest('instrumentation hooks', async () => { - await GET('/odata/v4/admin/Books(251)', admin) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - log.clear() - await GET('/odata/v4/admin/Books(252)', admin) - // primitive check that console has no trace logs - expect(log.output).not.to.match(/telemetry/) - }) + xtest('instrumentation hooks', async () => {}) test('$batch is traced', async () => { await POST( @@ -71,51 +65,48 @@ describe('tracing', () => { }, admin ) - // 4: POST: create/ new + read after write, GET: read actives + read drafts - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(4) + // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — + // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + + // read drafts) are now nested under 2 root tx spans, one per batch entry. + expect(rootSpans()).to.have.lengthOf(2) }) test('cds.spawn is traced', async () => { await POST('/odata/v4/admin/test_spawn', {}, admin) await wait(30) - // 2: action + spawned action - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(2) + // 2 visible roots: the action invocation + the spawned task + expect(rootSpans()).to.have.lengthOf(2) + expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) await wait(100) - // 1: local-messaging remains in same context - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(1) + // local-messaging keeps the consumer in the same context → exactly 1 visible root + expect(rootSpans()).to.have.lengthOf(1) }) describe('db', () => { describe('ql', () => { test('SELECT is traced', async () => { await SELECT.from('sap.capire.bookshop.Books') - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match( - /\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - READ sap\.capire\.bookshop\.Books/ - ) + expect(captured.some(s => s.name === 'db - READ sap.capire.bookshop.Books')).to.be.true }) }) test('native db statement is traced', async () => { const db = await cds.connect.to('db') await db.run('SELECT ID, title, stock, price FROM AdminService_Books WHERE ID = 201 OR ID = 207') - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match( - /\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - SELECT .* FROM AdminService_Books WHERE ID = 201 OR I…/ - ) + // The wrapper "db - SELECT …" span carries the raw SQL as part of the name. + expect(captured.some(s => s.name.startsWith('db - SELECT') && s.name.includes('AdminService_Books'))).to.be.true }) }) test('custom spans are supported', async () => { await GET('/odata/v4/catalog/ListOfBooks', {}, admin) await wait(100) - expect(log.output.match(/my custom span/g).length).to.equal(1) + expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1) }) // --- TODO --- From fdbd1bb759322221b666e9d0a69d431f0089afeb Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:15:44 +0200 Subject: [PATCH 06/17] chore: migrate test runner from jest to vitest (#474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the #437 stash. Migrates the test runner jest → vitest. ## Why it's a clean win Vitest with `pool: 'forks'` + per-file isolation tears each test file's child process down when the file finishes — so the OTLP exporter's lingering handles die with the child, and **the suite exits cleanly with no `--forceExit`**. This is the same open-handle class that hangs jest (see #472/#466). Verified: exit 0, ~8s, no hang across repeated runs. ## Config (`vitest.config.mjs`) - `globals: true` — `describe/test/beforeEach/...` stay available, test bodies unchanged. - `pool: 'forks'`, `isolate: true`, `teardownTimeout: 1000` — the clean-exit mechanism (documented inline). - Ports the old `jest.config.js` HANA logic faithfully: default `testTimeout: 42000`; under `CI && HANA_DRIVER` → `include` restricted to `tracing-attributes` + `passport`, timeout ×10, `cds_requires_telemetry_tracing` set when `HANA_PROM`. Verified the subset selection. ## Changes - `package.json`: `test` → `vitest run --silent`; jest removed, vitest added. `jest.config.js` deleted. - 8 `jest.spyOn`/`jest.fn` → `vi.spyOn`/`vi.fn`. - 5 `beforeAll(done => …)` hooks → promise-returning (vitest treats a hook arg as a fixture). - `eslint.config.mjs`: test-files override declaring `vi` global. Lint clean. - Lockfile regenerated against public npm (0 internal-registry URLs). ## ⚠️ Behavioral note — HTTP instrumentation disabled in the test app Under jest, OTel's `require-in-the-middle` http patching was **silently broken** by jest's module sandbox, so incoming HTTP SERVER spans never existed in tests (the existing `xtest` skips document this). Under vitest (real `require`) the instrumentation works and reparents trace trees, breaking several assertions. To keep this migration **behavior-neutral**, `test/bookshop/package.json` now sets `disableIncomingRequestInstrumentation` + `disableOutgoingRequestInstrumentation` on the http instrumentation — reproducing jest's effective environment. Consequence: the HTTP-instrumentation path stays untested (same blind spot as jest, now explicit config rather than an accident). **Follow-up issue filed to enable it and assert on the real incoming spans.** The tracing-attributes client-span assertions still pass because those spans come via undici / cloud-sdk, not instrumentation-http. ## Coordination Parallel PR #473 (prettier) touches `package.json` (additive) + lockfile. Lockfile will conflict — whichever merges second rebases. #473 excludes `jest.config.js` from formatting (this PR deletes it). ## Verified `npm run test` 53 pass / 14 skip, exit 0, ~8s, clean exit ×3 · `npm run lint` clean · HANA subset selection confirmed. --- eslint.config.mjs | 13 +- jest.config.js | 14 - package-lock.json | 6949 +++++++---------------- package.json | 4 +- test/bookshop/package.json | 4 +- test/logging.test.js | 9 +- test/metrics-outbox-disabled.test.js | 5 +- test/metrics-outbox-multitenant.test.js | 5 +- test/metrics-outbox.test.js | 7 +- test/tracing-attributes.test.js | 29 +- test/tracing-remote-cloudsdk.test.js | 25 +- test/tracing-remote-native.test.js | 25 +- test/tracing-span-names.test.js | 25 +- vitest.config.mjs | 37 + 14 files changed, 2175 insertions(+), 4976 deletions(-) delete mode 100644 jest.config.js create mode 100644 vitest.config.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index 0d250b28..79f04fff 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,2 +1,13 @@ import cds from '@sap/cds/eslint.config.mjs' -export default [...cds.recommended] +export default [ + ...cds.recommended, + { + // The cds eslint config declares jest/mocha test globals but not vitest's `vi`. + files: ['**/+(test|tests)/**/*.+(js|cjs|mjs)', '**/*.test.+(js|cjs|mjs)', '**/*-test.+(js|cjs|mjs)'], + languageOptions: { + globals: { + vi: 'readonly' + } + } + } +] diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 70fb6cd8..00000000 --- a/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const config = { - testTimeout: 42000, - testMatch: ['**/*.test.js'] -} - -if (process.env.CI && process.env.HANA_DRIVER) { - config.testTimeout *= 10 - config.testMatch = ['**/tracing-attributes.test.js', '**/passport.test.js'] - - if (process.env.HANA_PROM) - process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) -} - -module.exports = config diff --git a/package-lock.json b/package-lock.json index 434f7a0a..759d61b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,508 +37,12 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", - "jest": "^30.4.2" + "vitest": "^4" }, "peerDependencies": { "@sap/cds": "^10 || ^9" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", - "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, "node_modules/@cap-js/cds-test": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@cap-js/cds-test/-/cds-test-1.0.1.tgz", @@ -623,45 +127,11 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "devOptional": true, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -884,480 +354,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.4.1", - "jest-snapshot": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@sinonjs/fake-timers": "^15.4.0", - "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", - "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/types": "30.4.1", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1365,17 +361,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -1387,28 +372,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -2292,28 +1255,14 @@ "node": ">=14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", - "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, "funding": { - "url": "https://opencollective.com/pkgr" + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@protobufjs/aspromise": { @@ -2382,6 +1331,269 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sap-cloud-sdk/connectivity": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.8.0.tgz", @@ -2595,33 +1807,6 @@ "node": ">=18" } }, - "node_modules/@sinclair/typebox": { - "version": "0.34.52", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", - "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -2633,61 +1818,30 @@ "text-hex": "1.0.x" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } + "license": "MIT" }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } + "license": "MIT" }, "node_modules/@types/esrecurse": { "version": "4.3.1", @@ -2703,38 +1857,11 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "devOptional": true, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { @@ -2747,13 +1874,6 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -2761,718 +1881,619 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "safer-buffer": "~2.1.0" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=0.8" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "retry": "0.13.1" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/accepts": { + "node_modules/body-parser/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "peer": true, - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "devOptional": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=0.4.0" + "node": "20 || >=22" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "devOptional": true, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "debug": "4" - }, + "peer": true, "engines": { - "node": ">= 6.0.0" + "node": ">= 0.8" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "devOptional": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 0.4" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", + "peer": true, "dependencies": { - "type-fest": "^0.21.3" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ansi-regex": { + "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/chai-as-promised": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", + "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "check-error": "^2.1.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "chai": ">= 2.1.2 < 7" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">= 8" + "node": ">= 16" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=12" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "engines": { + "node": ">=8" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": "~2.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=0.8" + "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "retry": "0.13.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", - "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true, "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" + "engines": { + "node": ">=0.8" } }, - "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.4.1", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" + "node": ">=18" } }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "dev": true, "license": "MIT", "dependencies": { - "@types/babel__core": "^7.20.5" + "color-name": "^2.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" } }, - "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", - "babel-preset-current-node-syntax": "^1.2.0" + "color-name": "^2.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + "node": ">=14.6" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "devOptional": true, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=12.20" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.8" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "peer": true, - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, "engines": { "node": ">=18" }, @@ -3481,1445 +2502,254 @@ "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "peer": true, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "devOptional": true, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "peer": true, "engines": { - "node": "20 || >=22" + "node": ">= 0.6" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, + "peer": true, "engines": { - "node": ">=8" + "node": ">=6.6.0" } }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, "license": "MIT" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", - "peer": true, "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", + "peer": true, "engines": { - "node": ">=6" + "node": ">= 0.8" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, - "license": "MIT", - "peer": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/chai-as-promised": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", - "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "peer": true, "dependencies": { - "check-error": "^2.1.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "peerDependencies": { - "chai": ">= 2.1.2 < 7" + "engines": { + "node": ">= 0.4" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "safe-buffer": "^5.0.1" } }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", + "peer": true, "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "peer": true, "engines": { - "node": ">= 16" + "node": ">= 0.4" } }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT", - "peer": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.400", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", - "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT", - "peer": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", - "devOptional": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "devOptional": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=6" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } + "peer": true }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -4928,1096 +2758,801 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/eslint": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "devOptional": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "bin": { + "eslint": "bin/eslint.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.4.7" + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "devOptional": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "has-symbols": "^1.0.3" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "devOptional": true, + "license": "BSD-3-Clause", "dependencies": { - "function-bind": "^1.1.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "peer": true, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=4.0" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 6" + "node": ">=4.0" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", - "peer": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "devOptional": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "devOptional": true, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", + "peer": true, "engines": { - "node": ">= 4" + "node": ">= 0.6" } }, - "node_modules/import-in-the-middle": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", - "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, "license": "Apache-2.0", - "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - }, "engines": { - "node": ">=18" + "node": ">=12.0.0" } }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } + "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true, + "license": "MIT" }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "devOptional": true, + "license": "MIT" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">= 0.10" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "dev": true, "license": "MIT" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "devOptional": true, "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "devOptional": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT", - "peer": true - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=16" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "devOptional": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, - "license": "BSD-3-Clause", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { - "node": ">=10" + "node": ">= 6" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" } }, - "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", - "import-local": "^3.2.0", - "jest-cli": "30.4.2" - }, - "bin": { - "jest": "bin/jest.js" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/jest-changed-files": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", - "dev": true, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0", - "pretty-format": "30.4.1", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, + "license": "ISC", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "parse-json": "^5.2.0", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.4.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", - "dev": true, - "license": "MIT", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "devOptional": true, + "license": "ISC", "dependencies": { - "detect-newline": "^3.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10.13.0" } }, - "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", - "dev": true, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" + "node": ">= 0.4" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.4.7" }, "optionalDependencies": { - "fsevents": "^2.3.3" + "uglify-js": "^3.1.4" } }, - "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", - "dev": true, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" + "has-symbols": "^1.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "function-bind": "^1.1.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", - "dev": true, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", + "peer": true, "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" + "node": ">= 0.8" }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 6" } }, - "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", + "peer": true, "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", - "dev": true, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", - "jest-util": "30.4.1", - "jest-watcher": "30.4.1", - "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 4" } }, - "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", - "dev": true, - "license": "MIT", + "node_modules/import-in-the-middle": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "license": "Apache-2.0", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/jest-snapshot": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", - "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", - "dev": true, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-diff": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "pretty-format": "30.4.1", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">=0.8.19" } }, - "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", - "dev": true, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, + "peer": true, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.10" } }, - "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", - "dev": true, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", - "dev": true, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.4.1", - "string-length": "^4.0.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-worker": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", - "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.4.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.12.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, "node_modules/jks-js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.7.tgz", @@ -6030,40 +3565,6 @@ "node-rsa": "^1.1.1" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", - "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6071,13 +3572,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6092,19 +3586,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -6191,16 +3672,6 @@ "dev": true, "license": "MIT" }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6215,13 +3686,279 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6319,53 +4056,14 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -6404,13 +4102,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6465,16 +4156,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -6501,16 +4182,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -6523,20 +4194,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "bin": { - "napi-postinstall": "lib/cli.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { @@ -6593,16 +4267,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", @@ -6613,29 +4277,6 @@ "asn1": "^0.2.4" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6649,6 +4290,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6667,6 +4322,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -6681,22 +4337,6 @@ "fn.name": "1.x.x" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/opossum": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/opossum/-/opossum-10.0.0.tgz", @@ -6757,42 +4397,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6813,16 +4417,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6833,30 +4427,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -6868,6 +4438,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6888,83 +4465,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, "node_modules/prelude-ls": { @@ -6977,35 +4504,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/protobufjs": { "version": "7.6.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", @@ -7064,23 +4562,6 @@ "node": ">=6" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -7128,22 +4609,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7182,29 +4647,6 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -7212,7 +4654,40 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 4" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/router": { @@ -7269,16 +4744,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -7432,28 +4897,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/source-map": { "version": "0.6.1", @@ -7465,24 +4914,16 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -7493,28 +4934,12 @@ "node": "*" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -7526,6 +4951,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7536,209 +4968,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/synckit": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", - "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.3.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, "node_modules/systeminformation": { "version": "5.33.1", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", @@ -7766,87 +4995,56 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": "*" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "dev": true, - "license": "MIT" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -7881,14 +5079,6 @@ "node": ">= 14.0.0" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7902,29 +5092,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -7989,75 +5156,6 @@ "node": ">= 0.8" } }, - "node_modules/unrs-resolver": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.4" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.12.2", - "@unrs/resolver-binding-android-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-x64": "1.12.2", - "@unrs/resolver-binding-freebsd-x64": "1.12.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", - "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-musl": "1.12.2", - "@unrs/resolver-binding-openharmony-arm64": "1.12.2", - "@unrs/resolver-binding-wasm32-wasi": "1.12.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8075,21 +5173,6 @@ "dev": true, "license": "MIT" }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8115,6 +5198,174 @@ "node": ">=0.6.0" } }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/voca": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz", @@ -8122,16 +5373,6 @@ "dev": true, "license": "MIT" }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8148,6 +5389,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -8203,120 +5461,12 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "peer": true }, "node_modules/y18n": { "version": "5.0.8", @@ -8328,13 +5478,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/package.json b/package.json index d9c2d5a2..22f2968f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ ], "scripts": { "lint": "npx eslint . --max-warnings=0", - "test": "npx jest --silent" + "test": "vitest run --silent" }, "dependencies": { "@opentelemetry/api": "^1.9", @@ -47,7 +47,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", - "jest": "^30.4.2" + "vitest": "^4" }, "cds": { "requires": { diff --git a/test/bookshop/package.json b/test/bookshop/package.json index bf574fe9..7cabf2be 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -22,7 +22,9 @@ "instrumentations": { "http": { "config": { - "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js" + "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js", + "disableIncomingRequestInstrumentation": true, + "disableOutgoingRequestInstrumentation": true } } }, diff --git a/test/logging.test.js b/test/logging.test.js index 614b83dc..db8d1298 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -6,12 +6,19 @@ process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') +const wait = require('node:timers/promises').setTimeout + describe('logging', () => { const admin = { auth: { username: 'alice' } } const { dir } = console + // The queue's SchedulingService runs an initial outbox scan on server "listening"; its + // telemetry "elapsed times:" trace primer is exported asynchronously and would otherwise + // land in the spy window below. Drain it once up front before installing the spy. + // REVISIT: replace this fixed wait by polling for the primer / an in-memory exporter (see #478). + beforeAll(() => wait(500)) beforeEach(() => { - console.dir = jest.fn() + console.dir = vi.fn() }) afterAll(() => { console.dir = dir diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index 6864cc64..d1fe5bb1 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -49,4 +50,4 @@ describe('queue metrics is disabled', () => { expect(metricValue('med_storage_time_in_seconds')).to.eq(null) expect(metricValue('max_storage_time_in_seconds')).to.eq(null) }) -}) +}) \ No newline at end of file diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 41b46ee0..485a3380 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -221,4 +222,4 @@ describe('queue metrics for multi tenant service', () => { expect(metricValue(T2, 'remaining_entries')).to.eq(0) }) }) -}) +}) \ No newline at end of file diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index f87c3c67..68b9e66c 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -27,7 +28,7 @@ function metricValue(metric, queuedServiceName) { return mestRecentQueueMetricData.value } -const debugLog = (cds.log('telemetry').debug = jest.fn(() => {})) +const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { let totalInc = { [E1]: 0, [E2]: 0 } @@ -293,4 +294,4 @@ describe('queue metrics for single tenant service', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 03204b13..c1497b06 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -24,20 +24,21 @@ describe('tracing attributes', () => { describe('remote', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) - - afterAll(done => { - server.close(done) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) + + afterAll(() => new Promise(resolve => server.close(resolve))) test('HTTP client attributes are set on remote service span', async () => { // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 976ee348..3708cecf 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -11,7 +11,7 @@ describe('tracing remote via cloud sdk', () => { // cloud-sdk resilience module resolution has issues on cds 8 if (Number(cds.version.split('.')[0]) < 9) return - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) @@ -19,16 +19,19 @@ describe('tracing remote via cloud sdk', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 5a6947cc..fe8a0a2e 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -15,23 +15,26 @@ describe('tracing remote via native fetch', () => { // cloud-sdk resilience module resolution has issues on cds 8 if (Number(cds.version.split('.')[0]) < 9) return - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/test/tracing-span-names.test.js b/test/tracing-span-names.test.js index 027579f3..8ff39d39 100644 --- a/test/tracing-span-names.test.js +++ b/test/tracing-span-names.test.js @@ -5,7 +5,7 @@ const http = require('http') describe('span names', () => { beforeEach(data.reset) - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) @@ -63,16 +63,19 @@ describe('span names', () => { describe('cloud sdk', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 00000000..49b75cf5 --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,37 @@ +import { defineConfig } from 'vitest/config' + +// Default: 42s timeout, run every *.test.js file. +let testTimeout = 42000 +let include = ['test/**/*.test.js'] + +// HANA CI runs only a small subset with a 10x timeout (ported from the old +// jest.config.js). The `cds_requires_telemetry_tracing` env has to be set here, +// before any test file requires @sap/cds, so keep it in the config module. +if (process.env.CI && process.env.HANA_DRIVER) { + testTimeout *= 10 + include = ['test/**/tracing-attributes.test.js', 'test/**/passport.test.js'] + + if (process.env.HANA_PROM) + process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) +} + +export default defineConfig({ + test: { + // globals:true keeps describe/test/beforeEach/... available without importing + // them in every test file (smallest diff to the existing jest suite). + globals: true, + include, + testTimeout, + // The OTLP exporters (and CAP's telemetry SDK) can leave open handles/timers + // alive. Run each test file in its own forked child process so that, once a + // file finishes, its process is torn down and the handles die with it. This + // is what makes the suite EXIT CLEANLY where jest needed --forceExit. + // (In Vitest 4 the former poolOptions.forks.* are top-level options.) + pool: 'forks', + // fresh child per file: matches jest's per-file isolation and preserves the + // top-of-module process.env mutations some test files rely on. + isolate: true, + // don't hang the run waiting on lingering handles at teardown. + teardownTimeout: 5000 + } +}) From 205e449efb0498d4b5eb1f32795360d0c6d9120a Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:12:10 +0200 Subject: [PATCH 07/17] chore: adopt oxfmt for code formatting (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the #438 oxfmt spike — adopts **oxfmt** (`0.63.0`, pinned) properly. ## Config (`.oxfmtrc.jsonc`) Based on the #438 spike, verified against the `lib/*.js` house style: `singleQuote`, `semi: false`, `printWidth: 120`, `tabWidth: 2`, `trailingComma: none`, `arrowParens: avoid`. `ignorePatterns` excludes `*.md`, `node_modules`, `package-lock.json`, `CHANGELOG.md`, and `jest.config.js` (see coordination). ## Scripts - `format` → `npx oxfmt` (write is oxfmt's default) - `format:check` → `npx oxfmt --check` No git hook / husky / lint-staged — CI `format:check` + scripts only (the intrusive hook from the #438 spike is intentionally not carried over). ## Commits (reviewable split) 1. `chore: add oxfmt formatter tooling` — config + scripts + devDep + lockfile + CI step 2. `chore: apply oxfmt formatting` — repo-wide reformat (11 files, line-wrapping only; verified non-semantic via `git diff -w`) ## eslint coexistence `@sap/cds/eslint.config.mjs` is `recommended` + `no-unused-vars`/`no-console` only (no stylistic rules) → no conflict. `npm run lint` stays green. ## CI One line added to the `lint` job in `ci.yml`: `npm run format:check`. ## Verified `npm run format:check` ✅ (56 files) · `npm run lint` (--max-warnings=0) ✅ · `npm run test` → 53 pass / 14 skip, exit 0 · lockfile resolved from public npm (0 internal-registry URLs). ## Coordination Parallel PR #474 (jest→vitest) deletes `jest.config.js` — this PR excludes it from formatting (0-line diff confirmed) so no collision. `package.json` change here is additive (scripts + devDep); lockfile will conflict with #474 — whichever merges second rebases. --- .github/workflows/ci.yml | 1 + .oxfmtrc.jsonc | 21 ++ lib/exporter/ConsoleMetricExporter.js | 4 +- lib/logging/index.js | 5 +- lib/metrics/index.js | 8 +- lib/tracing/cloud_sdk.js | 12 +- lib/tracing/index.js | 8 +- package-lock.json | 410 ++++++++++++++++++++++++ package.json | 21 +- test/bookshop/srv/admin-service.js | 6 +- test/console-span-exporter.test.js | 72 ++++- test/metrics-outbox-disabled.test.js | 2 +- test/metrics-outbox-multitenant.test.js | 2 +- test/metrics-outbox.test.js | 2 +- test/tracing-attributes.test.js | 16 +- test/tracing-outboxed-batch.test.js | 24 +- test/tracing-remote-native.test.js | 4 +- 17 files changed, 574 insertions(+), 44 deletions(-) create mode 100644 .oxfmtrc.jsonc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79da765d..d9dd4939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: npm i - run: npm run lint + - run: npm run format:check test: runs-on: ubuntu-latest strategy: diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc new file mode 100644 index 00000000..413968f7 --- /dev/null +++ b/.oxfmtrc.jsonc @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "arrowParens": "avoid", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false, + "vueIndentScriptAndStyle": false, + "sortPackageJson": false, + "ignorePatterns": ["*.md", "node_modules/**", "package-lock.json", "CHANGELOG.md", "jest.config.js"] +} diff --git a/lib/exporter/ConsoleMetricExporter.js b/lib/exporter/ConsoleMetricExporter.js index 21493a3d..402fe6c0 100644 --- a/lib/exporter/ConsoleMetricExporter.js +++ b/lib/exporter/ConsoleMetricExporter.js @@ -119,7 +119,9 @@ class ConsoleMetricExporter extends StandardConsoleMetricExporter { // export other metrics for (const tenant of Object.keys(other)) { for (const [k, v] of Object.entries(other[tenant])) { - LOG.info(`${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}`) + LOG.info( + `${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}` + ) } } } diff --git a/lib/logging/index.js b/lib/logging/index.js index 08840201..45a1ea36 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -40,7 +40,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url config.credentials ??= credentials.credentials diff --git a/lib/metrics/index.js b/lib/metrics/index.js index bd9be759..f876433f 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -50,7 +50,8 @@ function _getExporter() { // Augment configuration depending on 'kind' of telemetry if (kind.match(/to-dynatrace$/)) { if (!credentials) credentials = getCredsForDTAsUPS() - if (!credentials) throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') + if (!credentials) + throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') config.url ??= `${credentials.apiurl}/v2/otlp/v1/metrics` config.headers ??= {} @@ -67,7 +68,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url diff --git a/lib/tracing/cloud_sdk.js b/lib/tracing/cloud_sdk.js index adc465fe..c19737c4 100644 --- a/lib/tracing/cloud_sdk.js +++ b/lib/tracing/cloud_sdk.js @@ -36,7 +36,11 @@ module.exports = () => { }) } }) - Object.defineProperty(cloudSDK, 'executeHttpRequest', { value: _executeHttpRequest, writable: true, configurable: true }) + Object.defineProperty(cloudSDK, 'executeHttpRequest', { + value: _executeHttpRequest, + writable: true, + configurable: true + }) const _executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { wrapper: function executeHttpRequestWithOrigin(destination, requestConfig) { return trace(_cloudSdkSpanName(destination, requestConfig), _executeWithOrigin, this, arguments, { @@ -45,5 +49,9 @@ module.exports = () => { }) } }) - Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { value: _executeHttpRequestWithOrigin, writable: true, configurable: true }) + Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { + value: _executeHttpRequestWithOrigin, + writable: true, + configurable: true + }) } diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 8a887c3a..bbdef34e 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -106,7 +106,8 @@ function _getExporter() { if (kind.match(/to-dynatrace$/)) { if (!credentials) credentials = getCredsForDTAsUPS() - if (!credentials) throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') + if (!credentials) + throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') config.url ??= `${credentials.apiurl}/v2/otlp/v1/traces` config.headers ??= {} // credentials.rest_apitoken?.token is deprecated and only supported for compatibility reasons @@ -119,7 +120,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url config.credentials ??= credentials.credentials diff --git a/package-lock.json b/package-lock.json index 759d61b4..ef4c98c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", + "oxfmt": "0.63.0", "vitest": "^4" }, "peerDependencies": { @@ -1265,6 +1266,353 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz", + "integrity": "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz", + "integrity": "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz", + "integrity": "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz", + "integrity": "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz", + "integrity": "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz", + "integrity": "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz", + "integrity": "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz", + "integrity": "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz", + "integrity": "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz", + "integrity": "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz", + "integrity": "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz", + "integrity": "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz", + "integrity": "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz", + "integrity": "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz", + "integrity": "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz", + "integrity": "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz", + "integrity": "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz", + "integrity": "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz", + "integrity": "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4365,6 +4713,58 @@ "node": ">= 0.8.0" } }, + "node_modules/oxfmt": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.63.0.tgz", + "integrity": "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.63.0", + "@oxfmt/binding-android-arm64": "0.63.0", + "@oxfmt/binding-darwin-arm64": "0.63.0", + "@oxfmt/binding-darwin-x64": "0.63.0", + "@oxfmt/binding-freebsd-x64": "0.63.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", + "@oxfmt/binding-linux-arm64-gnu": "0.63.0", + "@oxfmt/binding-linux-arm64-musl": "0.63.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", + "@oxfmt/binding-linux-riscv64-musl": "0.63.0", + "@oxfmt/binding-linux-s390x-gnu": "0.63.0", + "@oxfmt/binding-linux-x64-gnu": "0.63.0", + "@oxfmt/binding-linux-x64-musl": "0.63.0", + "@oxfmt/binding-openharmony-arm64": "0.63.0", + "@oxfmt/binding-win32-arm64-msvc": "0.63.0", + "@oxfmt/binding-win32-ia32-msvc": "0.63.0", + "@oxfmt/binding-win32-x64-msvc": "0.63.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5036,6 +5436,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", diff --git a/package.json b/package.json index 22f2968f..14c10c4c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ ], "scripts": { "lint": "npx eslint . --max-warnings=0", - "test": "vitest run --silent" + "test": "vitest run --silent", + "format": "npx oxfmt", + "format:check": "npx oxfmt --check" }, "dependencies": { "@opentelemetry/api": "^1.9", @@ -47,6 +49,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", + "oxfmt": "0.63.0", "vitest": "^4" }, "cds": { @@ -108,8 +111,12 @@ }, "telemetry-to-dynatrace": { "vcap": [ - { "label": "dynatrace" }, - { "tag": "dynatrace" } + { + "label": "dynatrace" + }, + { + "tag": "dynatrace" + } ], "tracing": { "exporter": { @@ -127,8 +134,12 @@ }, "telemetry-to-cloud-logging": { "vcap": [ - { "label": "cloud-logging" }, - { "tag": "Cloud Logging" } + { + "label": "cloud-logging" + }, + { + "tag": "Cloud Logging" + } ], "tracing": { "exporter": { diff --git a/test/bookshop/srv/admin-service.js b/test/bookshop/srv/admin-service.js index 9cb186fe..f8f4f3bd 100644 --- a/test/bookshop/srv/admin-service.js +++ b/test/bookshop/srv/admin-service.js @@ -43,11 +43,7 @@ module.exports = class AdminService extends cds.ApplicationService { this.on('test_outboxed_send_batch', async () => { const externalOne = await cds.connect.to('ExternalServiceOne') const queued = cds.queued(externalOne) - await Promise.all([ - queued.send('call', {}), - queued.send('call', {}), - queued.send('call', {}) - ]) + await Promise.all([queued.send('call', {}), queued.send('call', {}), queued.send('call', {})]) }) // test_scheduled: schedules a one-shot task to fire after a short delay. diff --git a/test/console-span-exporter.test.js b/test/console-span-exporter.test.js index 4c83e4e8..3fbee4fc 100644 --- a/test/console-span-exporter.test.js +++ b/test/console-span-exporter.test.js @@ -80,9 +80,30 @@ describe('ConsoleSpanExporter', () => { // childB (5 → 9 ms) const TRACE = 'a'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) - const childA = span({ name: 'childA', traceId: TRACE, spanId: 'cA', parentSpanId: 'r0', startMs: 1, durationMs: 3 }) - const grand = span({ name: 'grandchild', traceId: TRACE, spanId: 'g0', parentSpanId: 'cA', startMs: 2, durationMs: 1 }) - const childB = span({ name: 'childB', traceId: TRACE, spanId: 'cB', parentSpanId: 'r0', startMs: 5, durationMs: 4 }) + const childA = span({ + name: 'childA', + traceId: TRACE, + spanId: 'cA', + parentSpanId: 'r0', + startMs: 1, + durationMs: 3 + }) + const grand = span({ + name: 'grandchild', + traceId: TRACE, + spanId: 'g0', + parentSpanId: 'cA', + startMs: 2, + durationMs: 1 + }) + const childB = span({ + name: 'childB', + traceId: TRACE, + spanId: 'cB', + parentSpanId: 'r0', + startMs: 5, + durationMs: 4 + }) // Order matters: children must arrive BEFORE the root for the exporter's // temporaryStorage flush logic to merge them under the same primer. @@ -111,7 +132,14 @@ describe('ConsoleSpanExporter', () => { // Root starts at 100 ms wallclock; child at 105 ms. Child should display as 5.00 → ... const TRACE = 'b'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 100, durationMs: 20 }) - const child = span({ name: 'child', traceId: TRACE, spanId: 'c0', parentSpanId: 'r0', startMs: 105, durationMs: 10 }) + const child = span({ + name: 'child', + traceId: TRACE, + spanId: 'c0', + parentSpanId: 'r0', + startMs: 105, + durationMs: 10 + }) const [primer] = exportAndCapture([child, root]) @@ -123,8 +151,22 @@ describe('ConsoleSpanExporter', () => { const TRACE = 'c'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 50 }) const late = span({ name: 'late', traceId: TRACE, spanId: 's3', parentSpanId: 'r0', startMs: 10, durationMs: 1 }) - const earlyLong = span({ name: 'earlyLong', traceId: TRACE, spanId: 's1', parentSpanId: 'r0', startMs: 0, durationMs: 30 }) - const earlyShort = span({ name: 'earlyShort', traceId: TRACE, spanId: 's2', parentSpanId: 'r0', startMs: 0, durationMs: 5 }) + const earlyLong = span({ + name: 'earlyLong', + traceId: TRACE, + spanId: 's1', + parentSpanId: 'r0', + startMs: 0, + durationMs: 30 + }) + const earlyShort = span({ + name: 'earlyShort', + traceId: TRACE, + spanId: 's2', + parentSpanId: 'r0', + startMs: 0, + durationMs: 5 + }) const [primer] = exportAndCapture([late, earlyShort, earlyLong, root]) @@ -154,7 +196,14 @@ describe('ConsoleSpanExporter', () => { const TRACE = 'f'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) // The skip regex is /^[A-Z]+ \/\${0,1}\w+$/ — single path segment, no slashes after the first. - const noisy = span({ name: 'GET /catalog', traceId: TRACE, spanId: 'h0', parentSpanId: 'r0', startMs: 1, durationMs: 5 }) + const noisy = span({ + name: 'GET /catalog', + traceId: TRACE, + spanId: 'h0', + parentSpanId: 'r0', + startMs: 1, + durationMs: 5 + }) const [primer] = exportAndCapture([noisy, root]) @@ -215,7 +264,14 @@ describe('ConsoleSpanExporter', () => { it('does not throw when a child arrives without its parent (orphan trace)', () => { // No root provided for this trace — the exporter should buffer the child and not flush. const TRACE = '5'.repeat(32) - const orphan = span({ name: 'orphan', traceId: TRACE, spanId: 'o0', parentSpanId: 'r-missing', startMs: 0, durationMs: 1 }) + const orphan = span({ + name: 'orphan', + traceId: TRACE, + spanId: 'o0', + parentSpanId: 'r-missing', + startMs: 0, + durationMs: 1 + }) expect(() => exportAndCapture([orphan])).not.to.throw() expect(infoCalls.length).to.equal(0) diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index d1fe5bb1..2616a09e 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -50,4 +50,4 @@ describe('queue metrics is disabled', () => { expect(metricValue('med_storage_time_in_seconds')).to.eq(null) expect(metricValue('max_storage_time_in_seconds')).to.eq(null) }) -}) \ No newline at end of file +}) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 485a3380..0f0b459b 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -222,4 +222,4 @@ describe('queue metrics for multi tenant service', () => { expect(metricValue(T2, 'remaining_entries')).to.eq(0) }) }) -}) \ No newline at end of file +}) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 68b9e66c..113a76ee 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -294,4 +294,4 @@ describe('queue metrics for single tenant service', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index c1497b06..5972584f 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -79,14 +79,18 @@ describe('tracing attributes', () => { test('SELECT', async () => { await SELECT.from('sap.capire.bookshop.Books').where('title !=', 'DUMMY') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(5) _match_db_spans('SELECT') }) test('INSERT', async () => { await INSERT.into('sap.capire.bookshop.Books').entries([{ ID: 1 }, { ID: 2 }]) - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(2) // TODO // _match_db_spans('INSERT') @@ -94,7 +98,9 @@ describe('tracing attributes', () => { test('UPDATE', async () => { await UPDATE('sap.capire.bookshop.Books').set({ stock: 42 }).where('ID > 250') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(3) // TODO // _match_db_spans('UPDATE') @@ -102,7 +108,9 @@ describe('tracing attributes', () => { test('DELETE', async () => { await DELETE.from('sap.capire.bookshop.Books') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(0) // texts expect(rowCounts).to.include(5) // TODO diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index 640689ea..d55993ec 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -36,16 +36,18 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) // Look for a queue worker root containing multiple dispatch tx spans. - const workerTrace = groupedByTrace().find(g => - g.root.name === 'cds.spawn - run task' && - g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + const workerTrace = groupedByTrace().find( + g => g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 ) expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… - const lockTxs = workerTrace.all.filter(s => - s.name === 'db - tx' && - workerTrace.all.some(c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages') + const lockTxs = workerTrace.all.filter( + s => + s.name === 'db - tx' && + workerTrace.all.some( + c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' + ) ) expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) @@ -54,8 +56,14 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) for (const tx of dispatchTxs) { const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) - expect(kids.some(k => k.name.match(/ExternalServiceOne - handle/)), 'dispatch tx should contain handle call').to.be.true - expect(kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), 'dispatch tx should contain DELETE').to.be.true + expect( + kids.some(k => k.name.match(/ExternalServiceOne - handle/)), + 'dispatch tx should contain handle call' + ).to.be.true + expect( + kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), + 'dispatch tx should contain DELETE' + ).to.be.true } // The dispatch txs should overlap in time (parallel), not be strictly sequential. diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index fe8a0a2e..36c00beb 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -47,9 +47,7 @@ describe('tracing remote via native fetch', () => { // no mock handler - let it make the actual HTTP call via native fetch await remote.send({ method: 'GET', path: '/test' }) - const undiciSpan = getSpans().find( - s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici' - ) + const undiciSpan = getSpans().find(s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici') expect(undiciSpan, 'no span from @opentelemetry/instrumentation-undici').to.exist expect(undiciSpan.attributes['http.request.method']).to.equal('GET') expect(undiciSpan.attributes['http.response.status_code']).to.equal(200) From d8f3dd11b1743246c0c862e4947a313544ec1a1f Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:06:36 +0200 Subject: [PATCH 08/17] test: capture outbox+console metrics via in-memory reader & unit-test ConsoleMetricExporter (#479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Consolidates all outbox/metrics test-quality work into one PR (formerly split as #479 + the stacked #480). - **In-memory metric reader** — `test/bookshop/lib/MyInMemoryMetricReader.js`, the metrics counterpart to `MyInMemorySpanExporter` (#465). Mirrors production **DELTA** temporality: SUM counters are accumulated across flushes into per-series running totals; GAUGE datapoints keep the latest absolute value. Wired via the `metrics-outbox`, `metrics-outbox-disabled`, and `metrics` profiles in `.cdsrc.json`. - **Outbox suites off console spying** — the three `metrics-outbox*.test.js` suites drop the `console.dir` spy and fixed `wait()` sleeps in favor of the reader + an `expectEventually()` force-flush polling helper (fails fast if the meter provider isn't wired). Folds in #445's polling approach. - **ConsoleMetricExporter unit test** — new `test/console-metric-exporter.test.js`, a pure unit test of the exporter's formatting (db.pool table, queue table, other single-vs-array, tenant variants, host-metrics aggregation, shutdown→FAILED), mirroring `console-span-exporter.test.js`. - **`metrics.test.js`** converted from scraping `cds.test.log()` output to asserting on the in-memory reader's datapoints. Metrics testing now mirrors the tracing side exactly: a to-console unit test **plus** in-memory-exporter–based integration tests. ## Why Follow-up to #465 (span test infra): eliminate console/log spying in the metrics suite and give `ConsoleMetricExporter` direct unit coverage. ## Review addressed - Bot review triaged: explicit `COUNTER_METRIC_NAMES` dispatch for `isCounter`; real wall-clock debounce in the multitenant test; isolation NOTE on the module-level singletons. - Dropped the unused debug-log silencer in the multitenant suite (never asserted). Kept the single-tenant `debugLog` mock — it backs a real `unknown service` assertion. Test-only change (no `lib/` change), so no CHANGELOG entry — consistent with #465/#474/#476. closes #478 Supersedes #445 and #480 (both folded in here) — I'll close them once this merges. --- test/bookshop/.cdsrc.json | 12 +- test/bookshop/lib/MyInMemoryMetricReader.js | 189 +++++++++++++ test/console-metric-exporter.test.js | 255 +++++++++++++++++ test/metrics-outbox-disabled.test.js | 27 +- test/metrics-outbox-multitenant.test.js | 224 ++++++++------- test/metrics-outbox.test.js | 290 +++++++++++--------- test/metrics.test.js | 86 ++++-- 7 files changed, 807 insertions(+), 276 deletions(-) create mode 100644 test/bookshop/lib/MyInMemoryMetricReader.js create mode 100644 test/console-metric-exporter.test.js diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 6571cbe7..606f66f5 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -30,6 +30,10 @@ "metrics": { "config": { "exportIntervalMillis": 100 + }, + "exporter": { + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } @@ -46,8 +50,8 @@ "_db_pool": false, "_queue": true, "exporter": { - "module": "@opentelemetry/sdk-metrics", - "class": "ConsoleMetricExporter" + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } @@ -64,8 +68,8 @@ "_db_pool": false, "_queue": false, "exporter": { - "module": "@opentelemetry/sdk-metrics", - "class": "ConsoleMetricExporter" + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } diff --git a/test/bookshop/lib/MyInMemoryMetricReader.js b/test/bookshop/lib/MyInMemoryMetricReader.js new file mode 100644 index 00000000..5787ee01 --- /dev/null +++ b/test/bookshop/lib/MyInMemoryMetricReader.js @@ -0,0 +1,189 @@ +// In-memory metric reader for tests. Exported metrics are accumulated in a module-level array +// that tests can import directly via `require('./lib/MyInMemoryMetricReader').captured`. +// Wired into the meter provider via .cdsrc.json profile config (no provider-poking from tests): +// the class is exporter-shaped (has `export()`), so lib/metrics/index.js wraps it in a +// PeriodicExportingMetricReader — keeping the configured `exportIntervalMillis` working. +// +// Kept dependency-light on purpose: it does NOT `require('@sap/cds')` at module top (doing so +// broke span capture once for the sibling span exporter). Only @opentelemetry primitives. +// +// TEMPORALITY: mirrors production. lib/metrics/index.js configures the real exporter with +// `temporalityPreference: AggregationTemporality.DELTA`, so the tests must validate what a real +// DELTA export produces — the reader honors that preference rather than forcing CUMULATIVE. +// +// Under DELTA each export reports only the *increment* since the previous collection, and +// `expectEventually` force-flushes repeatedly, so a naive "latest datapoint" read of a counter +// would drop to 0 after the first flush. We therefore split handling by datapoint type: +// * SUM datapoints (the 3 counters: incoming_messages, outgoing_messages, processing_failures) +// are summed into a running total per counter series (metric name + full attribute set) — +// reconstructing the cumulative value the tests assert against (totalInc/totalOut/totalFailed). +// * GAUGE datapoints (cold_entries, remaining_entries, *_storage_time_in_seconds) are absolute +// point-in-time observations; for those we keep the latest exported value, never a sum. + +const { ExportResultCode } = require('@opentelemetry/core') +const { AggregationTemporality, DataPointType } = require('@opentelemetry/sdk-metrics') +const { metrics } = require('@opentelemetry/api') + +// Raw ResourceMetrics objects, one per collection/flush. Drives the GAUGE latest-value lookup. +const captured = [] + +// Running totals for SUM (counter) series. Keyed by the fully-qualified series identity +// (metric name + every attribute on the datapoint) so distinct (queue.name, tenant) series never +// collide; each entry keeps the original attributes so lookups can match by attribute subset the +// same way the gauge path does. Under DELTA the SDK reports the increment since its last +// collection; summing every increment a series receives reconstructs its cumulative value — which +// is what the tests track (totalInc/totalOut/totalFailed grow monotonically, never reset per case). +// +// NOTE: `captured` and `counterSeries` are process-level singletons. Cross-file correctness relies +// on Vitest isolating each test file in its own worker process (vitest.config.mjs: pool:'forks' + +// isolate:true). Two files sharing this module in one process would bleed counter totals together. +const counterSeries = new Map() + +function seriesKey(metricName, attributes) { + const sorted = Object.keys(attributes) + .sort() + .map(k => `${k}=${attributes[k]}`) + .join('&') + return `${metricName} ${sorted}` +} + +// True when `sub` is an attribute subset of `full` (all keys present with equal values). +function attributesMatch(full, sub) { + return Object.entries(sub).every(([key, value]) => full[key] === value) +} + +class MyInMemoryMetricReader { + constructor(config = {}) { + // Honor the temporality the plugin config sets (DELTA in production) so the tests exercise the + // real export shape. Defaults to DELTA to match lib/metrics/index.js when no config is passed. + this._temporality = config.temporalityPreference ?? AggregationTemporality.DELTA + } + + // Invoked by PeriodicExportingMetricReader for each instrument type. + selectAggregationTemporality() { + return this._temporality + } + + export(resourceMetrics, resultCallback) { + captured.push(resourceMetrics) + + // Fold DELTA increments of SUM (counter) datapoints into the running totals. + for (const scopeMetrics of resourceMetrics.scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if (metric.dataPointType !== DataPointType.SUM) continue + for (const dp of metric.dataPoints) { + const key = seriesKey(metric.descriptor.name, dp.attributes) + const entry = counterSeries.get(key) + if (entry) entry.total += dp.value + else counterSeries.set(key, { name: metric.descriptor.name, attributes: dp.attributes, total: dp.value }) + } + } + } + + resultCallback({ code: ExportResultCode.SUCCESS }) + } + + shutdown() { + return Promise.resolve() + } + + forceFlush() { + return Promise.resolve() + } +} + +// Most recent GAUGE MetricData for `queue.` that carries datapoints, scanning captured +// exports newest-first (mirrors the old `consoleDirLogs.findLast(... && dataPoints?.length)`). +function latestGaugeMetric(metricName) { + const name = `queue.${metricName}` + for (let i = captured.length - 1; i >= 0; i--) { + for (const scopeMetrics of captured[i].scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if ( + metric.descriptor.name === name && + metric.dataPointType === DataPointType.GAUGE && + metric.dataPoints?.length + ) + return metric + } + } + } + return null +} + +// Accumulated counter total for `queue.` across all series whose attributes match the +// given filter (subset match, like the gauge lookup). Returns null when no counter series exists +// for that name — i.e. the metric was never exported as a counter (queue metrics disabled) or the +// filter matches nothing. +function counterTotal(metricName, attributes) { + const name = `queue.${metricName}` + let found = false + let total = 0 + for (const entry of counterSeries.values()) { + if (entry.name === name && attributesMatch(entry.attributes, attributes)) { + found = true + total += entry.total + } + } + return found ? total : null +} + +// Names of metrics that are SUM (counter) instruments — the three counters the queue plugin +// registers. Dispatches latestDataPointValue explicitly, rather than relying on counterSeries +// happening to be populated (which is empty on the first poll after reset()). +const COUNTER_METRIC_NAMES = new Set([ + 'queue.incoming_messages', + 'queue.outgoing_messages', + 'queue.processing_failures' +]) + +function isCounter(metricName) { + return COUNTER_METRIC_NAMES.has(`queue.${metricName}`) +} + +// Value of `queue.` for the datapoint(s) matching all given attributes +// (e.g. { 'queue.name': ... } and/or { 'sap.tenancy.tenant_id': ... }). For counters this is the +// accumulated running total (cumulative, reconstructed from DELTA increments); for gauges it is +// the latest absolute observation. Returns null when the metric was never exported (queue metrics +// disabled) or no datapoint matches the filter. +function latestDataPointValue(metricName, attributes = {}) { + if (isCounter(metricName)) return counterTotal(metricName, attributes) + + const metric = latestGaugeMetric(metricName) + if (!metric) return null + const dp = metric.dataPoints.find(dp => attributesMatch(dp.attributes, attributes)) + return dp ? dp.value : null +} + +// Force the wired meter provider to collect + export now, so the reader reflects the latest state. +// Fails fast if the provider isn't the real (wired) one — a NoopMeterProvider has no forceFlush, +// which would otherwise silently no-op and let a polling helper busy-spin its whole timeout. +async function forceFlush() { + const provider = metrics.getMeterProvider() + if (typeof provider.forceFlush !== 'function') { + throw new Error( + 'MyInMemoryMetricReader.forceFlush: meter provider is not wired up (no forceFlush) — ' + + 'is the metrics-outbox profile active and the reader configured?' + ) + } + await provider.forceFlush() +} + +// Clears the per-test GAUGE state (captured exports) so a stale point-in-time value from a previous +// case cannot leak. The counter running totals are intentionally NOT cleared: the suites' counter +// assertions (totalInc/totalOut/totalFailed) and the plugin's underlying counters are cumulative +// across the whole file, and the SDK's DELTA baseline likewise persists across flushes — zeroing +// only our side would desync it and under-count. See the module header for the full rationale. +function reset() { + captured.length = 0 +} + +module.exports = { + MyInMemoryMetricReader, + captured, + counterSeries, + latestGaugeMetric, + latestDataPointValue, + forceFlush, + reset +} diff --git a/test/console-metric-exporter.test.js b/test/console-metric-exporter.test.js new file mode 100644 index 00000000..ab13af1f --- /dev/null +++ b/test/console-metric-exporter.test.js @@ -0,0 +1,255 @@ +// Unit tests for ConsoleMetricExporter — verifies the user-friendly formatting of the three +// output branches (db.pool table, queue table, "other" metrics) plus the aggregated host-metrics +// block, by feeding the exporter crafted ResourceMetrics-shaped fixtures and inspecting the +// formatted strings passed to LOG.info. +// +// This is a pure unit test: no cds.test server, no real OTel SDK, no console spying. + +const cds = require('@sap/cds') + +// Hook LOG.info BEFORE requiring the exporter so the exporter's module-level +// `cds.log('telemetry')` resolves to a logger whose .info we control. +const infoCalls = [] +const telemetryLog = cds.log('telemetry') +const originalInfo = telemetryLog.info +telemetryLog.info = (...args) => infoCalls.push(args) + +const ConsoleMetricExporter = require('../lib/exporter/ConsoleMetricExporter') + +afterAll(() => { + telemetryLog.info = originalInfo +}) + +beforeEach(() => { + infoCalls.length = 0 +}) + +// --- helpers --------------------------------------------------------------- + +// Builds a minimal ScopeMetrics-shaped object. +function scopeMetrics(name, metrics) { + return { scope: { name }, metrics } +} + +// Builds a minimal MetricData-shaped object. `dataPoints` are `{ attributes, value }`. +function metric(name, dataPoints, description = name) { + return { descriptor: { name, description }, dataPoints } +} + +// Drives the exporter and returns the lines logged. Asserts the result callback got SUCCESS. +function exportAndCapture(scopes) { + const exporter = new ConsoleMetricExporter() + let result + exporter.export({ scopeMetrics: scopes }, r => (result = r)) + expect(result).to.deep.equal({ code: 0 /* ExportResultCode.SUCCESS */ }) + return infoCalls.map(args => args[0]) +} + +// --- assertions ------------------------------------------------------------ + +const { expect } = require('@cap-js/cds-test') + +const APP_SCOPE = '@cap-js/telemetry' +const HOST_SCOPE = '@opentelemetry/instrumentation-host-metrics' + +describe('ConsoleMetricExporter', () => { + describe('db.pool table', () => { + it('renders a "db.pool:" header and the size/available/pending table row', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('db.pool.size', [{ attributes: {}, value: 3 }]), + metric('db.pool.max', [{ attributes: {}, value: 10 }]), + metric('db.pool.available', [{ attributes: {}, value: 2 }]), + metric('db.pool.pending', [{ attributes: {}, value: 1 }]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^db\.pool:/) + // Column header + expect(line).to.include('size | available | pending') + // size/max, available/size, pending — padded into the row + expect(line).to.match(/3\/10 \| +2\/3 \| +1/) + }) + + it('labels the table with the tenant id when a datapoint carries sap.tenancy.tenant_id', () => { + const attributes = { 'sap.tenancy.tenant_id': 't1' } + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('db.pool.size', [{ attributes, value: 5 }]), + metric('db.pool.max', [{ attributes, value: 8 }]), + metric('db.pool.available', [{ attributes, value: 4 }]), + metric('db.pool.pending', [{ attributes, value: 0 }]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^db\.pool of tenant "t1":/) + expect(line).to.match(/5\/8 \| +4\/5 \| +0/) + }) + }) + + describe('queue table', () => { + it('renders a "queue:" header, the wide column header, and lands the values', () => { + const dp = value => [{ attributes: {}, value }] + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('queue.cold_entries', dp(1)), + metric('queue.remaining_entries', dp(2)), + metric('queue.min_storage_time_in_seconds', dp(3)), + metric('queue.med_storage_time_in_seconds', dp(4)), + metric('queue.max_storage_time_in_seconds', dp(5)), + metric('queue.incoming_messages', dp(6)), + metric('queue.outgoing_messages', dp(7)), + metric('queue.processing_failures', dp(8)) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^queue:/) + // Column header (all eight columns) + expect(line).to.include( + 'cold | remaining | min storage time | med storage time | max storage time | incoming | outgoing | failed' + ) + // The eight values land in the padded row, in column order. + const row = line.split('\n').at(-1) + expect(row.split('|').map(c => c.trim())).to.deep.equal(['1', '2', '3', '4', '5', '6', '7', '8']) + }) + + it('labels the queue table with the tenant id when present', () => { + const attributes = { 'sap.tenancy.tenant_id': 't2' } + const dp = value => [{ attributes, value }] + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('queue.cold_entries', dp(0)), + metric('queue.remaining_entries', dp(0)), + metric('queue.min_storage_time_in_seconds', dp(0)), + metric('queue.med_storage_time_in_seconds', dp(0)), + metric('queue.max_storage_time_in_seconds', dp(0)), + metric('queue.incoming_messages', dp(0)), + metric('queue.outgoing_messages', dp(0)), + metric('queue.processing_failures', dp(0)) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^queue of tenant "t2":/) + }) + }) + + describe('other metrics', () => { + it('logs a single-datapoint metric unwrapped (inspect of the datapoint object)', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [metric('nodejs.eventloop.utilization', [{ attributes: {}, value: 0.42 }])]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + // Unwrapped: inspect(v[0]) of a single datapoint object → starts with "{" + expect(line).to.match(/^nodejs\.eventloop\.utilization: \{/) + expect(line).to.include('value: 0.42') + expect(line).not.to.match(/^nodejs\.eventloop\.utilization: \[/) + }) + + it('logs a multi-datapoint metric as an array (inspect of the datapoints array)', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('nodejs.eventloop.time', [ + { attributes: { 'nodejs.eventloop.state': 'active' }, value: 100 }, + { attributes: { 'nodejs.eventloop.state': 'idle' }, value: 200 } + ]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + // Wrapped: inspect(v) of the datapoints array → starts with "[" + expect(line).to.match(/^nodejs\.eventloop\.time: \[/) + expect(line).to.include('value: 100') + expect(line).to.include('value: 200') + }) + + it('labels other metrics with the tenant id when present', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [metric('some.metric', [{ attributes: { 'sap.tenancy.tenant_id': 't3' }, value: 1 }])]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^some\.metric of tenant "t3": \{/) + }) + }) + + describe('host metrics', () => { + const original = process.env.HOST_METRICS_LOG_SYSTEM + + afterEach(() => { + if (original === undefined) delete process.env.HOST_METRICS_LOG_SYSTEM + else process.env.HOST_METRICS_LOG_SYSTEM = original + }) + + // process.* metrics are always aggregated; a system.network.* metric is only aggregated when + // HOST_METRICS_LOG_SYSTEM is set. + function hostScope() { + return [ + scopeMetrics(HOST_SCOPE, [ + metric('process.cpu.time', [{ attributes: { 'process.cpu.state': 'user' }, value: 1.5 }], 'process cpu time'), + metric('process.memory.usage', [{ attributes: {}, value: 123456 }], 'process memory usage'), + metric( + 'system.network.io', + [{ attributes: { device: 'eth0', direction: 'receive' }, value: 999 }], + 'system network io' + ) + ]) + ] + } + + it('aggregates only process.* into a "host metrics:" block when HOST_METRICS_LOG_SYSTEM is unset', () => { + delete process.env.HOST_METRICS_LOG_SYSTEM + + const [line] = exportAndCapture(hostScope()) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^host metrics:/) + expect(line).to.include('process cpu time') + expect(line).to.include('process memory usage') + // system.* excluded when the flag is unset + expect(line).not.to.include('system network io') + }) + + it('additionally aggregates system.* when HOST_METRICS_LOG_SYSTEM is set', () => { + process.env.HOST_METRICS_LOG_SYSTEM = 'true' + + const [line] = exportAndCapture(hostScope()) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^host metrics:/) + expect(line).to.include('process cpu time') + expect(line).to.include('process memory usage') + expect(line).to.include('system network io') + }) + }) + + describe('shutdown', () => { + it('returns FAILED via setImmediate when the exporter is shutting down', () => { + const exporter = new ConsoleMetricExporter() + exporter._shutdown = true + + return new Promise(resolve => { + exporter.export({ scopeMetrics: [] }, result => { + expect(result).to.deep.equal({ code: 1 /* ExportResultCode.FAILED */ }) + expect(infoCalls.length).to.equal(0) + resolve() + }) + }) + }) + }) +}) diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index 2616a09e..6b6e33a8 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -1,23 +1,13 @@ -import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) - const cds = require('@sap/cds') -const { setTimeout: wait } = require('node:timers/promises') + +// With queue metrics disabled (_queue: false in the metrics-outbox-disabled profile) the +// in-memory reader should never capture any `queue.*` datapoints. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') const { expect, GET } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox-disabled') function metricValue(metric) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` - )?.[0] - - if (!mostRecentMetricLog) return null - - return mostRecentMetricLog.dataPoints[0].value + return latestDataPointValue(metric) } describe('queue metrics is disabled', () => { @@ -35,12 +25,15 @@ describe('queue metrics is disabled', () => { externalServiceOne.before('*', () => {}) }) - beforeEach(() => (consoleDirLogs.length = 0)) + beforeEach(() => reset()) test('metrics are not collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) - await wait(150) // Wait for metrics to be collected + // Assert absence: with _queue disabled no queue.* instrument is ever registered, so nothing can + // be exported. Force a few export cycles (rather than a fixed sleep) to give the app every chance + // to emit a queue metric — none must appear. + for (let i = 0; i < 5; i++) await forceFlush() expect(metricValue('cold_entries')).to.eq(null) expect(metricValue('remaining_entries')).to.eq(null) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 0f0b459b..d97bc09f 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -1,13 +1,10 @@ -import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) - const cds = require('@sap/cds') const { setTimeout: wait } = require('node:timers/promises') +// Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the +// metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET, axios } = cds.test( __dirname + '/bookshop', '--with-mocks', @@ -17,19 +14,36 @@ const { expect, GET, axios } = cds.test( axios.defaults.validateStatus = () => true function metricValue(tenant, metric) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` - )?.[0] - - if (!mostRecentMetricLog) return null + return latestDataPointValue(metric, { 'sap.tenancy.tenant_id': tenant }) +} - const mostRecentTenantDataPoint = mostRecentMetricLog.dataPoints.find( - dp => dp.attributes['sap.tenancy.tenant_id'] === tenant - ) - return mostRecentTenantDataPoint ? mostRecentTenantDataPoint.value : null +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces all fixed-time `wait(…)` sleeps — the loop completes the instant the in-memory +// per-tenant queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted +// state. forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails +// loudly instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } } describe('queue metrics for multi tenant service', () => { + if (cds.version.split('.')[0] < 9) { + test.skip('skipping tests for cds version < 9', () => {}) + return + } + const T1 = 'tenant_1' const T2 = 'tenant_2' @@ -67,7 +81,7 @@ describe('queue metrics for multi tenant service', () => { beforeEach(async () => { await cds.tx({ tenant: T1 }, () => DELETE.from('cds.outbox.Messages')) await cds.tx({ tenant: T2 }, () => DELETE.from('cds.outbox.Messages')) - consoleDirLogs.length = 0 + reset() }) describe('given the target service succeeds immediately', () => { @@ -77,34 +91,39 @@ describe('queue metrics for multi tenant service', () => { GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - await wait(150) // Wait for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + await expectEventually(() => { + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + }) }) }) describe('given a target service that requires retries', () => { let currentRetryCount, unboxedService + // Fail the first 3 attempts so the 4th delivers — the same widened window #445 introduced for + // the single-tenant suite: it opens a comfortable gap between "message has aged >=1s in the + // queue" and "message is delivered and removed", which is what made the wall-clock test flaky. + const ATTEMPTS_TO_FAIL = 3 + beforeAll(async () => { unboxedService = await cds.connect.to('ExternalServiceOne') unboxedService.before('call', req => { - if ((currentRetryCount[cds.context.tenant] += 1) <= 2) { + if ((currentRetryCount[cds.context.tenant] += 1) <= ATTEMPTS_TO_FAIL) { totalFailed[cds.context.tenant] += 1 return req.reject({ status: 503 }) } @@ -120,76 +139,75 @@ describe('queue metrics for multi tenant service', () => { }) test('storage time increases before message can be delivered', async () => { + // Reference time taken BEFORE the GETs so the queuing round-trip counts toward the wall-clock debounce below. const timeOfInitialCall = Date.now() await Promise.all([ GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T1]), GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - // Wait for the first retry to be processed - while (currentRetryCount[T1] < 2) await wait(10) - while (currentRetryCount[T2] < 2) await wait(10) - - // Wait until at least 1 second has passed since the initial call - const timeAfterFirstRetry = Date.now() - if (timeAfterFirstRetry - timeOfInitialCall < 1000) { - await wait(1000 - (timeAfterFirstRetry - timeOfInitialCall)) - } - await wait(150) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(1) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.be.gte(1) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(1) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.be.gte(1) - - // Wait for the second retry to be processd - while (currentRetryCount[T1] < 3) await wait(10) - while (currentRetryCount[T2] < 3) await wait(10) - await wait(600) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + // The storage_time gauges need a real second to elapse since the messages were enqueued — + // this is the one place the test fundamentally depends on wall-clock time. + const elapsed = Date.now() - timeOfInitialCall + if (elapsed < 1500) await wait(1500 - elapsed) + + await expectEventually(() => { + // Message is still being retried (>=1s aged) for both tenants. + expect(currentRetryCount[T1]).to.be.gte(2) + expect(currentRetryCount[T2]).to.be.gte(2) + + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(1) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.be.gte(1) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(1) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.be.gte(1) + }) + + // Final attempt — the message is delivered and removed from the outbox for both tenants. + await expectEventually(() => { + expect(currentRetryCount[T1]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + expect(currentRetryCount[T2]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + }) }) }) describe('given a taget service that fails unrecoverably', () => { let unboxedService - const didProcess = { [T1]: false, [T2]: false } - beforeAll(async () => { unboxedService = await cds.connect.to('ExternalServiceOne') unboxedService.before('call', req => { - didProcess[cds.context.tenant] = true totalFailed[cds.context.tenant] += 1 return req.reject({ status: 418, unrecoverable: true }) }) @@ -205,21 +223,19 @@ describe('queue metrics for multi tenant service', () => { GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - while (!didProcess[T1]) await wait(10) - while (!didProcess[T2]) await wait(10) - await wait(500) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(1) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(1) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) + await expectEventually(() => { + expect(metricValue(T1, 'cold_entries')).to.eq(1) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(1) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + }) }) }) }) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 113a76ee..ab9f0d95 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -1,9 +1,4 @@ import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) const E1 = 'ExternalServiceOne' const E2 = 'ExternalServiceTwo' @@ -11,26 +6,46 @@ const E2 = 'ExternalServiceTwo' const cds = require('@sap/cds') const { setTimeout: wait } = require('node:timers/promises') +// Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the +// metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET, axios } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox') axios.defaults.validateStatus = () => true function metricValue(metric, queuedServiceName) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` && metricLog[0].dataPoints?.length - )?.[0] - - const mestRecentQueueMetricData = mostRecentMetricLog?.dataPoints.find( - dataPoint => dataPoint.attributes['queue.name'] === queuedServiceName - ) - - if (!mestRecentQueueMetricData) return null + return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) +} - return mestRecentQueueMetricData.value +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory +// queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted state. +// forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly +// instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } } const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { + if (cds.version.split('.')[0] < 9) { + test.skip('skipping tests for cds version < 9', () => {}) + return + } + let totalInc = { [E1]: 0, [E2]: 0 } let totalOut = { [E1]: 0, [E2]: 0 } let totalFailed = { [E1]: 0, [E2]: 0 } @@ -74,7 +89,7 @@ describe('queue metrics for single tenant service', () => { beforeEach(async () => { await DELETE.from('cds.outbox.Messages') - consoleDirLogs.length = 0 + reset() debugLog.mockClear() }) @@ -82,37 +97,43 @@ describe('queue metrics for single tenant service', () => { test('metrics are collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) - await wait(150) // Wait for metrics to be collected - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + }) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - await wait(150) // Wait for metrics to be collected - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) describe('given a target service that requires retries', () => { let currentRetryCount, customizedHandler + // Fail the first 3 attempts so the 4th delivers. With the queue's exp-backoff schedule + // (0.5s, 1.25s, 2.375s, ...), this places the 4th attempt at ~t=4.1s after enqueue — + // giving a comfortable ~3s window between "message has aged 1s in the queue" and + // "message is finally delivered and removed". Tightening that window is what made the + // original wall-clock-based test flaky. + const ATTEMPTS_TO_FAIL = 3 const customizedHandlerFor = E => req => { - if ((currentRetryCount[E] += 1) <= 2) { + if ((currentRetryCount[E] += 1) <= ATTEMPTS_TO_FAIL) { totalFailed[E] += 1 return req.reject({ status: 503 }) } @@ -142,88 +163,87 @@ describe('queue metrics for single tenant service', () => { test('storage time increases before message can be delivered', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - + // Reference time taken after GETs return — i.e. after both messages are persisted in the outbox. const timeOfInitialCall = Date.now() - await wait(150) // ... for metrics to be collected - expect(currentRetryCount[E1]).to.eq(1) - expect(currentRetryCount[E2]).to.eq(1) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) - - // Wait for the first retry to be initiated - while (currentRetryCount[E1] < 2) await wait(10) - while (currentRetryCount[E2] < 2) await wait(10) - await wait(150) // ... for the retry to be processed and metrics to be collected - expect(currentRetryCount[E1]).to.eq(2) - expect(currentRetryCount[E2]).to.eq(2) - - // Wait until at least 1 second has passed since the initial call - const timeAfterFirstRetry = Date.now() - if (timeAfterFirstRetry - timeOfInitialCall < 1000) { - await wait(1000 - (timeAfterFirstRetry - timeOfInitialCall)) - } + // The queue has made its first delivery attempt for both services (handler invocation count is + // observed directly via the rejecting `before('call')` handler — pure CAP event observation). + await expectEventually(() => { + expect(currentRetryCount[E1]).to.be.gte(1) + expect(currentRetryCount[E2]).to.be.gte(1) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(1) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(1) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) - await wait(150) // ... for metrics to be collected again - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.be.gte(1) - expect(metricValue('med_storage_time_in_seconds', E1)).to.be.gte(1) - expect(metricValue('max_storage_time_in_seconds', E1)).to.be.gte(1) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.be.gte(1) - expect(metricValue('med_storage_time_in_seconds', E2)).to.be.gte(1) - expect(metricValue('max_storage_time_in_seconds', E2)).to.be.gte(1) - - // Wait for the second retry to be initiated - while (currentRetryCount[E1] < 3) await wait(10) - while (currentRetryCount[E2] < 3) await wait(10) - await wait(150) // ... for the retry to be processed and metrics to be collected - expect(currentRetryCount[E1]).to.eq(3) - expect(currentRetryCount[E2]).to.eq(3) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + // The storage_time gauges need a real second to elapse since the messages were enqueued — + // this is the one place the test fundamentally depends on wall-clock time. + const elapsed = Date.now() - timeOfInitialCall + if (elapsed < 1500) await wait(1500 - elapsed) + + await expectEventually(() => { + // Either still on attempt 2 (waiting to retry) or on attempt 3 (delivered) — both are fine + // for these assertions, the message has been in the queue >=1s either way. + expect(currentRetryCount[E1]).to.be.gte(2) + expect(currentRetryCount[E2]).to.be.gte(2) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(1) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.be.gte(1) + expect(metricValue('med_storage_time_in_seconds', E1)).to.be.gte(1) + expect(metricValue('max_storage_time_in_seconds', E1)).to.be.gte(1) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(1) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.be.gte(1) + expect(metricValue('med_storage_time_in_seconds', E2)).to.be.gte(1) + expect(metricValue('max_storage_time_in_seconds', E2)).to.be.gte(1) + }) + + // Final attempt — the message is delivered and removed from the outbox. + await expectEventually(() => { + expect(currentRetryCount[E1]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + expect(currentRetryCount[E2]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) @@ -256,25 +276,25 @@ describe('queue metrics for single tenant service', () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - await wait(150) // ... for metrics to be collected - - expect(metricValue('cold_entries', E1)).to.eq(1) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(1) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E1)).to.eq(1) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(1) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) diff --git a/test/metrics.test.js b/test/metrics.test.js index 78765d24..6582b7a3 100644 --- a/test/metrics.test.js +++ b/test/metrics.test.js @@ -1,36 +1,90 @@ -// process.env.HOST_METRICS_RETAIN_SYSTEM = 'true' //> with this the test would fail -process.env.HOST_METRICS_LOG_SYSTEM = 'true' +// Integration tests for metrics collection — asserts on what is actually COLLECTED (which +// instruments produce datapoints, and how many), captured in-memory by MyInMemoryMetricReader +// (wired via the `metrics` profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's +// log output. The formatting of those metrics is unit-tested in console-metric-exporter.test.js. const cds = require('@sap/cds') +const { setTimeout: wait } = require('node:timers/promises') + +const { captured, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'metrics') -const log = cds.test.log() -const wait = require('node:timers/promises').setTimeout +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces fixed-time sleeps — the loop completes the instant the captured datapoints +// reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a +// misconfigured profile fails loudly instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// All metric descriptor names present across every captured export. +function capturedMetricNames() { + const names = new Set() + for (const rm of captured) { + for (const scopeMetrics of rm.scopeMetrics) { + for (const metric of scopeMetrics.metrics) names.add(metric.descriptor.name) + } + } + return names +} + +// Most recent captured MetricData for the given descriptor name (newest export first). +function latestMetric(name) { + for (let i = captured.length - 1; i >= 0; i--) { + for (const scopeMetrics of captured[i].scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if (metric.descriptor.name === name && metric.dataPoints?.length) return metric + } + } + } + return null +} describe('metrics', () => { const admin = { auth: { username: 'alice' } } - beforeEach(log.clear) + beforeEach(reset) test('system metrics are not collected by default', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - await wait(100) - - expect(log.output).to.match(/process/i) - expect(log.output).not.to.match(/network/i) + await expectEventually(() => { + const names = capturedMetricNames() + // process.* host metrics ARE collected out of the box ... + expect([...names].some(n => n.startsWith('process.'))).to.be.true + // ... but system.* (network/cpu/memory) collection is NOT enabled by default. + expect([...names].some(n => n.startsWith('system.'))).to.be.false + expect([...names].some(n => n.includes('network'))).to.be.false + }) }) - test('other metrics with multiple datapoints are logged as array', async () => { + test('other metrics can carry multiple datapoints', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - await wait(200) - - // nodejs.eventloop.time has multiple datapoints (active + idle) → logged as array - expect(log.output).to.match(/nodejs\.eventloop\.time: \[/) - // nodejs.eventloop.utilization has single datapoint → logged unwrapped (not as array) - expect(log.output).to.match(/nodejs\.eventloop\.utilization: \{/) + await expectEventually(() => { + // nodejs.eventloop.time is collected with multiple datapoints (active + idle) ... + const time = latestMetric('nodejs.eventloop.time') + expect(time).to.exist + expect(time.dataPoints.length).to.be.greaterThan(1) + // ... whereas nodejs.eventloop.utilization is a single datapoint. + const utilization = latestMetric('nodejs.eventloop.utilization') + expect(utilization).to.exist + expect(utilization.dataPoints.length).to.equal(1) + }) }) }) From eb28f9b92129e163f111e4f9575a775737a84c16 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:01:49 +0200 Subject: [PATCH 09/17] chore(deps-dev): bump the OTLP dev-dependency group to 0.221 (pin sdk-logs, #482) (#483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Recreates dependabot #472 against `develop`, bumping the OTLP dev-dependency group to 0.221: - `@opentelemetry/exporter-metrics-otlp-grpc`: `^0.219` → `^0.221` - `@opentelemetry/exporter-metrics-otlp-proto`: `^0.219` → `^0.221` - `@opentelemetry/exporter-trace-otlp-grpc`: `^0.219` → `^0.221` - `@opentelemetry/exporter-trace-otlp-proto`: `^0.219` → `^0.221` - `@opentelemetry/instrumentation-host-metrics`: `^0.2.0` → `^0.4.0` - `@opentelemetry/instrumentation-runtime-node`: `^0.32.0` → `^0.34.0` ## The pin Adds a top-level `overrides` block pinning `@opentelemetry/sdk-logs` to `0.219.0`: ```json "overrides": { "@opentelemetry/sdk-logs": "0.219.0" } ``` `@opentelemetry/sdk-logs` 0.221 introduces an unbounded memory leak that OOM-crashes `test/logging.test.js` (heap climbs to ~3.8GB, crash after ~110s). Root-caused and tracked in #482. The pin keeps sdk-logs at 0.219 while everything else moves to 0.221, so `logging.test.js` behaves like `develop` again (passes in ~5s). Remove the pin once #482 is fixed. ## Verification - `test/logging.test.js`: passes in ~4.9s, exits promptly (no OOM/hang) - Full suite: 63 passed / 14 skipped, exits cleanly in ~7.4s - Lint (`eslint . --max-warnings=0`) + format (`oxfmt --check`): clean - Lock resolves sdk-logs to `0.219.0` while `exporter-trace-otlp-proto` is `0.221.0` — pin is surgical Supersedes #472. No CHANGELOG entry (dev-deps only). Refs #482. --- package-lock.json | 576 ++++++---------------------------------------- package.json | 15 +- 2 files changed, 77 insertions(+), 514 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef4c98c5..3d2554fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,12 @@ "@cap-js/sqlite": "^3", "@cap-js/telemetry": "file:.", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.219", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.219", - "@opentelemetry/exporter-trace-otlp-proto": "^0.219", - "@opentelemetry/instrumentation-host-metrics": "^0.2.0", - "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.221", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221", + "@opentelemetry/exporter-trace-otlp-proto": "^0.221", + "@opentelemetry/instrumentation-host-metrics": "^0.4.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", @@ -422,20 +422,15 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz", - "integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -444,68 +439,18 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz", - "integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -514,69 +459,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz", - "integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -585,70 +477,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz", - "integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -657,69 +496,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz", - "integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -728,57 +514,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/instrumentation": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", @@ -797,13 +532,13 @@ } }, "node_modules/@opentelemetry/instrumentation-host-metrics": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.2.0.tgz", - "integrity": "sha512-NIttCEOLdg1ebbDiJpCf0Ly1OGIa10isesik+K2dnXy2P99q4muUFjpaLtTnhkENrt9SmR0Zrxzq7B+W/VNWyw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.4.0.tgz", + "integrity": "sha512-jnFyX2sTn2B+9mjsL3qgAHzpxdaia4/FV2GSlf9QrpaiMi6O0M0lA9JZTsf4FTvuOwrIngCk+MeVXjsxgBXXyg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.219.0", + "@opentelemetry/instrumentation": "^0.221.0", "systeminformation": "^5.31.6" }, "engines": { @@ -813,37 +548,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", - "integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-http": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", @@ -863,46 +567,15 @@ } }, "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.32.0.tgz", - "integrity": "sha512-Jo1jSgrHlah3lPpGNPsIpF0q52D5uSLRJrztWUoPc1/Tli2ZWZ+cArgNtcdmiLuKhW21MwYbbcrNw1fbOPeR3A==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.34.0.tgz", + "integrity": "sha512-Yb3PcmuK/iIOWY49GSEcSGl7fR4r6UqhZnuy5EWvFaqKqqs9SYnQeyQUEAbnBnSougRpwFBDbdN1SSGcvMhbtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.219.0", + "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.219.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", - "integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/instrumentation": "^0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -929,14 +602,14 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", - "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-transformer": "0.219.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -945,33 +618,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz", - "integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -980,35 +637,19 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", - "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-logs": "0.219.0", - "@opentelemetry/sdk-metrics": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1017,87 +658,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/resources": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", diff --git a/package.json b/package.json index 14c10c4c..3c52a465 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,12 @@ "@cap-js/sqlite": "^3", "@cap-js/telemetry": "file:.", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.219", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.219", - "@opentelemetry/exporter-trace-otlp-proto": "^0.219", - "@opentelemetry/instrumentation-host-metrics": "^0.2.0", - "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.221", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221", + "@opentelemetry/exporter-trace-otlp-proto": "^0.221", + "@opentelemetry/instrumentation-host-metrics": "^0.4.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", @@ -52,6 +52,9 @@ "oxfmt": "0.63.0", "vitest": "^4" }, + "overrides": { + "@opentelemetry/sdk-logs": "0.219.0" + }, "cds": { "requires": { "telemetry": { From 8f8485dcf485b017677863848dd8f93046ac7d0f Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:53:40 +0200 Subject: [PATCH 10/17] =?UTF-8?q?test:=20run=20full=20suite=20on=20HANA=20?= =?UTF-8?q?+=20fix=20two=20HANA=20span/metric=20bugs=20(#477=20=C2=A72,=20?= =?UTF-8?q?=C2=A75)=20(#481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Started as removing dead test exclusions for #477 (§2 cds<9 guards, §5 HANA CI test-subset). Removing the HANA subset surfaced HANA-only failures — including **two real production bugs** that the old 2-file subset had been masking — so this PR also fixes those. ### Production fixes (lib/) - **fix(tracing):** raw SQL leaked into HANA `INSERT` `prepare` span names. The name-normalization regex used `.` which doesn't match newlines, so HANA's multi-line `INSERT … WITH SRC AS (…)` SQL survived in the span name. Now uses `[\s\S]` so it's stripped to operation + table (matching SELECT); the SQL stays in `db.query.text`. - **fix(metrics):** `*_storage_time_in_seconds` gauges were skewed by the machine's UTC offset on HANA — HANA's `min()`/`max()` aggregates return timezone-naive timestamps that `new Date()` parsed as local time. Normalize to UTC before parsing. Both carry CHANGELOG `### Fixed` entries. ### Test-suite changes (§2/§5 + HANA robustness) - Remove all 8 dead `cds < 9` guards (§2) and the HANA CI 2-file test-subset (§5) so the full suite runs on HANA. - Convert queue/outbox span assertions to force-flush + poll (spans export after fixed waits on slower HANA); filter the outbox-scan trace primer out of the logging assertion; fix a lifecycle bug where a retry handler fired with an undefined counter. - **HANA CI config** (`vitest.config.mjs`, HANA-only): run files serially (all files share one HDI container vs sqlite's per-file in-memory DB), raise `hookTimeout`, HANA-only outbox settle in `afterAll`, and `retry: 2` for residual shared-remote-container timing variance. sqlite unchanged (retry:0, full parallelism). ### Skips (deviation from "no skips except passport" — conscious) - Multitenancy tests (`tracing-mt`, `metrics-outbox-multitenant`) **skip on HANA** with an explanatory comment: they need a bound BTP Service Manager for MTX tenant subscription, which the single pre-provisioned HDI container in CI doesn't provide. They still run fully on sqlite. This means multitenancy has no HANA coverage — acknowledged; can be revisited if the CI HANA setup gains MTX. ## Verification - sqlite: 63 passed / 14 skipped / 0 failed. - HANA (serial + retry:2): 64 passed / 6 skipped / 0 failed, stable across repeated runs (the 6 skips = 2 multitenancy + 4 pre-existing xtest/TODO in tracing.test.js). Refs #477 (§1 queue-worker sqlite skips remain, gated on the cds queue-spawn fix; §3 stubs pending). --- CHANGELOG.md | 2 + lib/metrics/queue.js | 29 +++- lib/tracing/trace.js | 4 +- test/bookshop/.cdsrc.json | 2 +- test/logging.test.js | 13 +- test/metrics-outbox-multitenant.test.js | 8 +- test/metrics-outbox.test.js | 103 ++++++++----- test/tracing-attributes.test.js | 3 - test/tracing-messaging-inboxed.test.js | 10 +- ...racing-messaging-persistent-outbox.test.js | 10 +- test/tracing-messaging.js | 94 ++++++++++-- test/tracing-mt.test.js | 3 + test/tracing-outboxed-batch.test.js | 135 +++++++++++------- test/tracing-remote-cloudsdk.test.js | 3 - test/tracing-remote-native.test.js | 3 - test/tracing-scheduled.test.js | 87 +++++++---- test/tracing.test.js | 67 +++++++-- vitest.config.mjs | 40 +++++- 18 files changed, 449 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39154001..23ba12a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) +- Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) +- Queue `*_storage_time_in_seconds` metrics are now correct on HANA (timezone-naive `min`/`max` timestamp aggregates were parsed as local time, skewing the values by the machine's UTC offset) ## Version 2.0.1 - 2026-07-03 diff --git a/lib/metrics/queue.js b/lib/metrics/queue.js index d6cbaa37..eb800b4d 100644 --- a/lib/metrics/queue.js +++ b/lib/metrics/queue.js @@ -6,6 +6,26 @@ const LOG = cds.log('telemetry') const PERSISTENT_QUEUE_DB_NAME = 'cds.outbox.Messages' +// Parse a queue `timestamp` value to epoch millis, robust to the DB driver's format. +// Direct column reads return an ISO-8601 UTC string ("...Z"), but HANA's min()/max() +// aggregates return a timezone-naive string ("2026-08-13 22:50:41.2270000" — space +// separator, sub-ms digits, no zone). Passing that straight to `new Date()` parses it as +// LOCAL time, so storage-time gauges were off by the machine's UTC offset on HANA (e.g. 7200s +// in CEST). Normalize naive strings to UTC before parsing; ISO/Date/number inputs pass through. +function timestampToEpoch(ts) { + if (ts == null) return null + if (ts instanceof Date) return ts.getTime() + if (typeof ts === 'number') return ts + let s = String(ts).trim() + // Already zoned (ends with Z or ±HH:MM / ±HHMM)? leave as-is; otherwise treat as UTC. + if (!/[zZ]$|[+-]\d\d:?\d\d$/.test(s)) { + // "YYYY-MM-DD HH:MM:SS.fffffff" -> "YYYY-MM-DDTHH:MM:SS.fffZ" (trim sub-ms to 3 digits) + s = s.replace(' ', 'T').replace(/(\.\d{3})\d+$/, '$1') + 'Z' + } + const ms = Date.parse(s) + return Number.isNaN(ms) ? null : ms +} + async function collectLatestQueueInfo(queueEntity, serviceName, maxAttempts) { const coldEntriesRow = await SELECT.one .columns([{ func: 'count', args: [{ val: 1 }], as: 'cold_count' }]) @@ -111,14 +131,17 @@ function initQueueObservation(statistics) { batchResult.observe(observables.remainingEntries, stats.remainingEntries, observationAttributes) // 'maxTimestamp' holds the most recent timestamp - const minStorageTimeSeconds = stats.maxTimestamp ? Math.floor((now - new Date(stats.maxTimestamp)) / 1000) : 0 + const maxEpoch = timestampToEpoch(stats.maxTimestamp) + const minStorageTimeSeconds = maxEpoch ? Math.floor((now - maxEpoch) / 1000) : 0 batchResult.observe(observables.minStorageTimeSeconds, minStorageTimeSeconds, observationAttributes) - const medStorageTimeSeconds = stats.medTimestamp ? Math.floor((now - new Date(stats.medTimestamp)) / 1000) : 0 + const medEpoch = timestampToEpoch(stats.medTimestamp) + const medStorageTimeSeconds = medEpoch ? Math.floor((now - medEpoch) / 1000) : 0 batchResult.observe(observables.medStorageTimeSeconds, medStorageTimeSeconds, observationAttributes) // 'minTimestamp' holds the least recent timestamp - const maxStorageTimeSeconds = stats.minTimestamp ? Math.floor((now - new Date(stats.minTimestamp)) / 1000) : 0 + const minEpoch = timestampToEpoch(stats.minTimestamp) + const maxStorageTimeSeconds = minEpoch ? Math.floor((now - minEpoch) / 1000) : 0 batchResult.observe(observables.maxStorageTimeInSeconds, maxStorageTimeSeconds, observationAttributes) batchResult.observe(observables.incomingMessages, stats.incomingMessages, observationAttributes) diff --git a/lib/tracing/trace.js b/lib/tracing/trace.js index dc9d70db..feb85fb7 100644 --- a/lib/tracing/trace.js +++ b/lib/tracing/trace.js @@ -313,7 +313,9 @@ function trace(req, fn, that, args, opts = {}) { // Matches "@cap-js/ - " optionally followed by " ", // where is prepare | exec | stmt.. Covers both the sqlite/pg case // (SQL is already baked in) and the HANA-promisified case (SQL not yet appended). - const dbNameMatch = name.match(/^(@cap-js\/\w+ - (?:prepare|exec|stmt\.\w+))(?:\s.*)?$/) + // Note: [\s\S] (not .) so multi-line SQL — e.g. HANA's INSERT ... WITH SRC AS (...) — + // is matched and stripped too; `.` alone would miss it and leak the raw statement. + const dbNameMatch = name.match(/^(@cap-js\/\w+ - (?:prepare|exec|stmt\.\w+))(?:\s[\s\S]*)?$/) if (dbNameMatch && (options.attributes[ATTR_DB_OPERATION_NAME] || options.attributes[ATTR_DB_SQL_TABLE])) { const SQL_VERB = { READ: 'SELECT', CREATE: 'INSERT' } const op = options.attributes[ATTR_DB_OPERATION_NAME] diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 606f66f5..7b046bcf 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -45,7 +45,7 @@ "telemetry": { "metrics": { "config": { - "exportIntervalMillis": 100 + "exportIntervalMillis": 1000 }, "_db_pool": false, "_queue": true, diff --git a/test/logging.test.js b/test/logging.test.js index db8d1298..84f06f3b 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -3,20 +3,19 @@ // REVISIT: even with profile "logging", cls_custom_fields from package.json wins process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) +// This test asserts the exported LogRecords only. Disable the tracing signal (no exporter → +// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed +// times:" trace primer is never produced and can't land in the console.dir spy window. Without +// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. +process.env.cds_requires_telemetry_tracing = JSON.stringify({ exporter: false }) + const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') -const wait = require('node:timers/promises').setTimeout - describe('logging', () => { const admin = { auth: { username: 'alice' } } const { dir } = console - // The queue's SchedulingService runs an initial outbox scan on server "listening"; its - // telemetry "elapsed times:" trace primer is exported asynchronously and would otherwise - // land in the spy window below. Drain it once up front before installing the spy. - // REVISIT: replace this fixed wait by polling for the primer / an in-memory exporter (see #478). - beforeAll(() => wait(500)) beforeEach(() => { console.dir = vi.fn() }) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index d97bc09f..e5115d18 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -38,12 +38,10 @@ async function expectEventually(assertion, { timeout = 10000, interval = 25 } = } } +// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. +// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this +// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). describe('queue metrics for multi tenant service', () => { - if (cds.version.split('.')[0] < 9) { - test.skip('skipping tests for cds version < 9', () => {}) - return - } - const T1 = 'tenant_1' const T2 = 'tenant_2' diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index ab9f0d95..dc079f6d 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -17,12 +17,35 @@ function metricValue(metric, queuedServiceName) { return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) } +// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI +// container a background queue worker may be holding the connection pool (draining/retrying), +// so a bare `DELETE` can block indefinitely — which previously turned into a 100s hook timeout +// that starved the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the +// DELETE against a short timeout and swallow errors: if it can't complete quickly, the leftover +// rows are handled by the next file's own beforeEach clear anyway. +async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining / server shutting down — nothing left to clean matters + } +} + // State-based wait: force the wired meter provider to collect + export, then re-run the assertion // block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory -// queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted state. +// queue statistics (kept fresh by the queue-stats cds.spawn poller) reflect the asserted state. // forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly // instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { +// +// interval is 500ms (NOT a few ms): each forceFlush() triggers a metric collection that runs the +// queue-stats poller's SELECTs against the DB. On the SHARED HANA HDI container a tight poll loop +// (plus the profile's background export) starves the queue worker of connections, so its retries +// stall and delivery never completes — which manifested as `expected N to be at least M` flakes +// and, via ensuing hook hangs + pool exhaustion, ECONNREFUSED cascades into later files' servers. +// Polling at 500ms (with the profile's exportIntervalMillis raised to 1000ms) leaves the worker +// enough DB headroom to make all its attempts. The loop still returns the instant the state holds, +// so sqlite (per-file in-memory DB) still satisfies in well under a second. +async function expectEventually(assertion, { timeout = 30000, interval = 500 } = {}) { const start = Date.now() let lastError while (true) { @@ -41,11 +64,6 @@ async function expectEventually(assertion, { timeout = 10000, interval = 25 } = const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { - if (cds.version.split('.')[0] < 9) { - test.skip('skipping tests for cds version < 9', () => {}) - return - } - let totalInc = { [E1]: 0, [E2]: 0 } let totalOut = { [E1]: 0, [E2]: 0 } let totalFailed = { [E1]: 0, [E2]: 0 } @@ -88,11 +106,26 @@ describe('queue metrics for single tenant service', () => { }) beforeEach(async () => { - await DELETE.from('cds.outbox.Messages') + await clearOutbox() reset() debugLog.mockClear() }) + // Leave the shared DB clean for the next test file and let background queue workers settle. + // On HANA all files share one HDI container, so (a) the undeliverable `unknown-service` row + // inserted by the last case below would otherwise linger and skew another file's queue metrics, + // and (b) an in-flight worker retrying a message could fire this file's `before('call')` handler + // during teardown. Clear, wait a beat for any in-flight worker iteration to finish, clear again. + // Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the hook. HANA-only: + // sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + afterAll(async () => { + await clearOutbox() + if (process.env.TELEMETRY_TEST_HANA) { + await wait(5000) + await clearOutbox() + } + }) + describe('given the target service succeeds immediately', () => { test('metrics are collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) @@ -124,7 +157,15 @@ describe('queue metrics for single tenant service', () => { }) describe('given a target service that requires retries', () => { - let currentRetryCount, customizedHandler + // Initialized at declaration (not left undefined): the `before('call')` handler registered in + // beforeAll stays live for the whole describe, so a background queue-worker retry can fire it + // OUTSIDE any test's window (between tests, or during teardown). If currentRetryCount were + // undefined then, `currentRetryCount[E]` throws — the queue logs "Programming error detected" + // and the delivery the test expects never completes. On HANA the slower retry cadence + pool + // drain at teardown reliably hits that gap; sqlite's timing never exposed it. beforeEach still + // re-zeroes it per test. + let currentRetryCount = { [E1]: 0, [E2]: 0 } + let customizedHandler // Fail the first 3 attempts so the 4th delivers. With the queue's exp-backoff schedule // (0.5s, 1.25s, 2.375s, ...), this places the 4th attempt at ~t=4.1s after enqueue — @@ -166,30 +207,26 @@ describe('queue metrics for single tenant service', () => { // Reference time taken after GETs return — i.e. after both messages are persisted in the outbox. const timeOfInitialCall = Date.now() - // The queue has made its first delivery attempt for both services (handler invocation count is - // observed directly via the rejecting `before('call')` handler — pure CAP event observation). - await expectEventually(() => { - expect(currentRetryCount[E1]).to.be.gte(1) - expect(currentRetryCount[E2]).to.be.gte(1) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) - }) + // Freshly-enqueued state: each message is present (remaining == 1) and not cold. We assert + // each service in its OWN poll (E1 and E2 stagger; coupling them risks one aging out before + // the other aligns). Storage_time is asserted as a small upper bound rather than exactly 0: + // the "just enqueued, ~0s old" state is a sub-second transient and on HANA the queue-stats + // poller's first observation already lands with storage_time >= 1 (poll interval + query + // latency), so `== 0` is not reliably observable. The `< 60` bound still guards the timezone + // regression this suite covers (a naive-timestamp misparse reported storage_time as ~7200s); + // storage-time GROWTH is asserted in the next block, delivery/removal in the one after. + const assertFreshlyEnqueued = E => + expectEventually(() => { + expect(metricValue('cold_entries', E)).to.eq(0) + expect(metricValue('remaining_entries', E)).to.eq(1) + expect(metricValue('incoming_messages', E)).to.eq(totalInc[E]) + expect(metricValue('outgoing_messages', E)).to.eq(totalOut[E]) + expect(metricValue('processing_failures', E)).to.eq(totalFailed[E]) + expect(metricValue('min_storage_time_in_seconds', E)).to.be.lessThan(60) + expect(metricValue('med_storage_time_in_seconds', E)).to.be.lessThan(60) + expect(metricValue('max_storage_time_in_seconds', E)).to.be.lessThan(60) + }) + await Promise.all([assertFreshlyEnqueued(E1), assertFreshlyEnqueued(E2)]) // The storage_time gauges need a real second to elapse since the messages were enqueued — // this is the one place the test fundamentally depends on wall-clock time. diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 5972584f..5b539128 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -41,9 +41,6 @@ describe('tracing attributes', () => { afterAll(() => new Promise(resolve => server.close(resolve))) test('HTTP client attributes are set on remote service span', async () => { - // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment - if (Number(cds.version.split('.')[0]) < 9) return - // configure destination URL directly on credentials cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } const remote = await cds.connect.to('TestRemote') diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index a5ec21e3..bd5fdb3d 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -54,13 +54,15 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { expect(rootSpans.length).to.be.lte(5) } -const cds = require('@sap/cds') - describe(`tracing messaging - ${CASE}`, () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans. + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } - require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) + require('./tracing-messaging')(CASE, CHECK) }) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 82b89109..708e8802 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -94,13 +94,15 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } } -const cds = require('@sap/cds') - describe(`tracing messaging - ${CASE}`, () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans. + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } - require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) + require('./tracing-messaging')(CASE, CHECK) }) diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 15202e34..147fc594 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -1,12 +1,66 @@ -module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { +module.exports = (CASE, CHECK) => { const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) - const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') + const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') + const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout + // Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI + // container a background queue worker may hold the connection pool, so a bare DELETE can block + // indefinitely — which would turn into a hook timeout that starves the pool and cascades into + // ECONNREFUSED for the next file's server. Race the DELETE against a short timeout; leftover + // rows are cleared by the next file's own beforeEach anyway. + async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining during shutdown — nothing left to clean matters + } + } + + // Force-flush the tracer provider's span processor so any spans buffered by background + // queue-worker activity are exported into `captured`. The global provider is a + // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; + // guard for the no-op provider so a misconfigured profile fails loudly, not silently. + async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() + } + + // State-based wait: repeatedly flush + re-run the assertion until it holds or times out. + // Replaces the fixed `wait(waitMs)` sleep that flakes on HANA, where the two queue workers + // flush their spans well after any reasonable fixed window. + async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } + } + const admin = { auth: { username: 'alice' } } + // The queue scheduler periodically scans `cds.outbox.Messages` in its own `db - tx` (a + // SELECT + optional UPDATE that finds nothing to dispatch). On HANA these bookkeeping scans + // land as extra root traces that have nothing to do with the emit under test — and because + // the single HDI container is shared across all test files, scans triggered by other files' + // lingering workers show up too. Filter those pure outbox-scan traces so the CHECKs' exact + // root-count assertions stay stable. A scan trace is a `db - tx` root whose every span only + // touches `cds.outbox.Messages` (no application entity, no messaging/handle span). + const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) + const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) + const rm = () => { try { require('fs').rmSync(require('path').join(__dirname, CASE)) @@ -21,22 +75,40 @@ module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { }) afterAll(async () => { - // Wait long enough for any background queue-worker / scheduling-service timers to - // fire one last time before jest tears down the env. Without this, those timers can - // fire after teardown and crash with "cds.error.isSystemError is not a function" - // (cds module is reloaded between tests, but the timer references the old instance). - await wait(2000) + // On the shared HANA HDI container, a still-draining background queue worker from THIS file + // would dispatch into the NEXT file's run and add foreign `cds.spawn - run task` roots that + // break its exact root-count CHECKs. Clear the shared outbox, let the last worker settle, then + // clear again. Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the + // hook. HANA-only: sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + if (process.env.TELEMETRY_TEST_HANA) { + await clearOutbox() + await wait(5000) + await clearOutbox() + } rm() }) - beforeEach(() => { + beforeEach(async () => { + // Clear any outbox rows left behind by a prior test file BEFORE resetting the span buffer. + // The single HANA HDI container is shared across all files, so a leftover message would be + // dispatched by THIS file's queue worker — producing a foreign `cds.spawn - run task` root + // that breaks the exact root-count CHECKs. Reset AFTER so the DELETE's own spans aren't + // captured. (No-op on sqlite, where each file gets its own in-memory DB.) + await clearOutbox() reset() }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(waitMs) - // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } - CHECK({ expect, rootSpans: rootSpans(), groupedByTrace: groupedByTrace(), captured: [...captured], cds }) + // Poll (flush + re-check) until both queue workers have run and exported their spans; + // on HANA the worker latency exceeds any reasonable fixed sleep. Pass the meaningful + // (non-outbox-scan) traces so the CHECK's exact root-count assertions aren't thrown off by + // the scheduler's bookkeeping scans on the shared HANA container. + await eventually(() => { + const groups = meaningful(groupedByTrace()) + const roots = groups.flatMap(g => g.roots) + // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } + CHECK({ expect, rootSpans: roots, groupedByTrace: groups, captured: [...captured], cds }) + }) }) } diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index 872a6dc2..8aa91e74 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -4,6 +4,9 @@ const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. +// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this +// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' const TENANT2 = 'tenant_2' diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index d55993ec..a1d8f9bc 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -6,16 +6,46 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') const { hrTimeToNanoseconds } = require('@opentelemetry/core') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout -describe('tracing for outboxed batch (chunk-size fan-out)', () => { - if (Number(cds.version.split('.')[0]) < 9) { - test.skip('skipping for cds < 9', () => {}) - return +// Force-flush the tracer provider's span processor so any spans buffered by background +// outbox/queue activity are exported into `captured`. The global provider is a +// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; +// guard for the no-op provider so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where background work flushes spans +// after the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } } +} + +describe('tracing for outboxed batch (chunk-size fan-out)', () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans (captured stays empty). + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } @@ -25,56 +55,65 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { externalOne.on('call', () => 'ok') }) - beforeEach(reset) + beforeEach(async () => { + // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA + // HDI container is shared across all files, so a leftover message would be dispatched by this + // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own + // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + await DELETE.from('cds.outbox.Messages') + reset() + }) test('three queued sends produce parallel dispatch spans under one worker root', async () => { await POST('/odata/v4/admin/test_outboxed_send_batch', {}, { auth: { username: 'alice' } }) - await wait(2500) - // Producer wrote three rows to the outbox. - const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') - expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) + await eventually(() => { + // Producer wrote three rows to the outbox. + const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') + expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) - // Look for a queue worker root containing multiple dispatch tx spans. - const workerTrace = groupedByTrace().find( - g => g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 - ) - expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist + // Look for a queue worker root containing multiple dispatch tx spans. + const workerTrace = groupedByTrace().find( + g => + g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + ) + expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist - // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… - const lockTxs = workerTrace.all.filter( - s => - s.name === 'db - tx' && - workerTrace.all.some( - c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' - ) - ) - expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) + // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… + const lockTxs = workerTrace.all.filter( + s => + s.name === 'db - tx' && + workerTrace.all.some( + c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' + ) + ) + expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) - // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. - const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') - expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) - for (const tx of dispatchTxs) { - const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) - expect( - kids.some(k => k.name.match(/ExternalServiceOne - handle/)), - 'dispatch tx should contain handle call' - ).to.be.true - expect( - kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), - 'dispatch tx should contain DELETE' - ).to.be.true - } + // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. + const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') + expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) + for (const tx of dispatchTxs) { + const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) + expect( + kids.some(k => k.name.match(/ExternalServiceOne - handle/)), + 'dispatch tx should contain handle call' + ).to.be.true + expect( + kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), + 'dispatch tx should contain DELETE' + ).to.be.true + } - // The dispatch txs should overlap in time (parallel), not be strictly sequential. - if (dispatchTxs.length >= 2) { - const sorted = [...dispatchTxs].sort( - (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) - ) - const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) - const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) - // Parallel: second starts before first ends (allow a tiny slack). - expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) - } + // The dispatch txs should overlap in time (parallel), not be strictly sequential. + if (dispatchTxs.length >= 2) { + const sorted = [...dispatchTxs].sort( + (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) + ) + const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) + const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) + // Parallel: second starts before first ends (allow a tiny slack). + expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) + } + }) }) }) diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 3708cecf..4adfcede 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -8,9 +8,6 @@ const http = require('http') // export so the outbound call produces a @cap-js/telemetry CLIENT span carrying // the sap.btp.destination attribute. describe('tracing remote via cloud sdk', () => { - // cloud-sdk resilience module resolution has issues on cds 8 - if (Number(cds.version.split('.')[0]) < 9) return - const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 36c00beb..f930a5d8 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -12,9 +12,6 @@ const http = require('http') // comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and // NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. describe('tracing remote via native fetch', () => { - // cloud-sdk resilience module resolution has issues on cds 8 - if (Number(cds.version.split('.')[0]) < 9) return - const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index a5928956..aa9c2a49 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -21,19 +21,49 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout -describe('tracing for scheduled tasks', () => { - if (Number(cds.version.split('.')[0]) < 9) { - test.skip('skipping for cds < 9', () => {}) - return +// Force-flush the tracer provider's span processor so any spans buffered by background +// queue/worker activity are exported into `captured`. The global provider is a +// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; +// guard for the no-op provider so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where the worker flushes spans after +// the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } } +} + +describe('tracing for scheduled tasks', () => { // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans (captured stays empty). + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } @@ -43,31 +73,40 @@ describe('tracing for scheduled tasks', () => { externalOne.on('call', () => 'ok') }) - beforeEach(reset) + beforeEach(async () => { + // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA + // HDI container is shared across all files, so a leftover message would be dispatched by this + // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own + // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + await DELETE.from('cds.outbox.Messages') + reset() + }) test('schedule .after() is fully traced through the queue worker', async () => { await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) - // wait long enough for the scheduled task to fire (10ms after-delay + worker latency) - await wait(1500) - // Producer trace: writes the task row inside the HTTP request tx. - const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) - expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') - expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true - expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + // Poll (flush + re-check) until the scheduled task has fired and all spans have been + // exported; on HANA the worker latency exceeds any reasonable fixed sleep. + await eventually(() => { + // Producer trace: writes the task row inside the HTTP request tx. + const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true - // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. - const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') - expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist - expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true - expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true + // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. + const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true + expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true - // The ExternalServiceOne handler was invoked. - expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true + // The ExternalServiceOne handler was invoked. + expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true - // Total meaningful roots: producer + worker (+ optional bookkeeping scan). - expect(rootSpans().length).to.be.gte(2) - expect(rootSpans().length).to.be.lte(3) + // Total meaningful roots: producer + worker (+ optional bookkeeping scan). + expect(rootSpans().length).to.be.gte(2) + expect(rootSpans().length).to.be.lte(3) + }) }) }) diff --git a/test/tracing.test.js b/test/tracing.test.js index b1cd985a..d81806c9 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -9,10 +9,50 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter // (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no // console spying, no string-regex matching of formatted output. -const { reset, rootSpans, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout +// Force-flush the tracer provider's span processor so any spans buffered by background +// activity are exported into `captured`. The global provider is a ProxyTracerProvider (no +// forceFlush) whose delegate is the real NodeTracerProvider; guard for the no-op provider +// so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where spawned/emitted work flushes +// spans after the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// On HANA the persistent-outbox queue poller periodically scans `cds.outbox.Messages` in its +// own `db - tx`, producing an extra root trace that is unrelated to what these tests exercise. +// Filter those bookkeeping traces out so root-count assertions stay stable across both DBs. +const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) +const meaningfulRoots = () => + groupedByTrace() + .filter(g => !isOutboxScanTrace(g)) + .flatMap(g => g.roots) + describe('tracing', () => { const admin = { auth: { username: 'alice' } } @@ -42,13 +82,19 @@ describe('tracing', () => { }) test('NonRecordingSpans are handled correctly', async () => { + // Idempotent cleanup: this file has no data.reset, and on the persistent HANA container a + // leftover Author 42 from a prior run would make the POST fail with a unique-constraint 500. + await DELETE.from('sap.capire.bookshop.Authors').where({ ID: 42 }) + reset() const { status: postStatus } = await POST('/odata/v4/admin/Authors', { ID: 42, name: 'Douglas Adams' }, admin) expect(postStatus).to.equal(201) const { status: getStatus } = await GET('/odata/v4/admin/Authors?$select=ID', admin) expect(getStatus).to.equal(200) // The sampler in this test ignores /odata/v4/admin/Authors — no spans should be captured for it. // (Other unrelated background work may still produce spans; assert only that none mention Authors.) - expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) + await eventually(() => { + expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) + }) }) // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible @@ -68,23 +114,23 @@ describe('tracing', () => { // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + // read drafts) are now nested under 2 root tx spans, one per batch entry. - expect(rootSpans()).to.have.lengthOf(2) + await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(2)) }) test('cds.spawn is traced', async () => { await POST('/odata/v4/admin/test_spawn', {}, admin) - await wait(30) // 2 visible roots: the action invocation + the spawned task - expect(rootSpans()).to.have.lengthOf(2) - expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true - expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true + await eventually(() => { + expect(meaningfulRoots()).to.have.lengthOf(2) + expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true + }) }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(100) // local-messaging keeps the consumer in the same context → exactly 1 visible root - expect(rootSpans()).to.have.lengthOf(1) + await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(1)) }) describe('db', () => { @@ -105,8 +151,7 @@ describe('tracing', () => { test('custom spans are supported', async () => { await GET('/odata/v4/catalog/ListOfBooks', {}, admin) - await wait(100) - expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1) + await eventually(() => expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1)) }) // --- TODO --- diff --git a/vitest.config.mjs b/vitest.config.mjs index 49b75cf5..987f3e1b 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,15 +1,30 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig, configDefaults } from 'vitest/config' // Default: 42s timeout, run every *.test.js file. let testTimeout = 42000 +let hookTimeout = 30000 let include = ['test/**/*.test.js'] +let exclude = configDefaults.exclude -// HANA CI runs only a small subset with a 10x timeout (ported from the old -// jest.config.js). The `cds_requires_telemetry_tracing` env has to be set here, -// before any test file requires @sap/cds, so keep it in the config module. -if (process.env.CI && process.env.HANA_DRIVER) { +// HANA CI runs the FULL suite (`test/**/*.test.js`, the default `include`) with a +// 10x test timeout since HANA is slower than sqlite. The `cds_requires_telemetry_tracing` +// env has to be set here, before any test file requires @sap/cds, so keep it in the +// config module. +const HANA = process.env.CI && process.env.HANA_DRIVER +if (HANA) { testTimeout *= 10 - include = ['test/**/tracing-attributes.test.js', 'test/**/passport.test.js'] + + // Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI + // containers. The HANA CI runs against a single pre-provisioned HDI container with no + // Service Manager, so these two suites can't run there — exclude them from the HANA job + // entirely (they still run on sqlite with in-memory tenants). + exclude = [...configDefaults.exclude, '**/tracing-mt.test.js', '**/metrics-outbox-multitenant.test.js'] + + // Signal "running on HANA" to test files that must branch at COLLECTION time (before + // cds.test() applies its --profile), e.g. the queue/outbox files that skip the sqlite-only + // cds.spawn cases. Reading cds.env at collection time would freeze the env singleton before + // the profile is applied, so files read this env var instead. + process.env.TELEMETRY_TEST_HANA = '1' if (process.env.HANA_PROM) process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) @@ -21,7 +36,14 @@ export default defineConfig({ // them in every test file (smallest diff to the existing jest suite). globals: true, include, + exclude, testTimeout, + hookTimeout, + // A couple of queue/outbox tests are timing-sensitive against the SHARED remote HANA Cloud + // HDI container (non-deterministic queue-worker latency); the afterAll settle reduces but + // can't fully remove the flakiness. Retry on HANA only so an unlucky timing miss self-heals; + // sqlite (per-file in-memory DB) is deterministic and gets no retries. + retry: HANA ? 2 : 0, // The OTLP exporters (and CAP's telemetry SDK) can leave open handles/timers // alive. Run each test file in its own forked child process so that, once a // file finishes, its process is torn down and the handles die with it. This @@ -31,6 +53,12 @@ export default defineConfig({ // fresh child per file: matches jest's per-file isolation and preserves the // top-of-module process.env mutations some test files rely on. isolate: true, + // On HANA every test file shares ONE HDI container (unlike sqlite's per-file + // in-memory DB), so files must not run concurrently: parallel workers collide on + // fixture INSERTs and on the shared cds.outbox.Messages table. Run files serially + // on HANA; the queue/outbox test files also clear the outbox in a beforeAll so a + // prior file's leftover rows can't bleed in. (sqlite keeps full parallelism.) + fileParallelism: !HANA, // don't hang the run waiting on lingering handles at teardown. teardownTimeout: 5000 } From bfb8bcd1f86d8758819a26e1f8f509131b6c0d7d Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:54:53 +0200 Subject: [PATCH 11/17] chore: release workflow (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Chore: Clean Up Release Workflow ♻️ **Refactor**: Removed an outdated workaround comment from the release workflow. ### Changes * `.github/workflows/release.yml`: Removed the `# REVISIT: remove "npm explore better-sqlite3 -- npm run install" with cds^10` comment that was no longer relevant, cleaning up the release workflow configuration. - [ ] 🔄 Regenerate and Update Summary
PR Bot Information **Version:** `1.29.26` - Output Template: [Default Template](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_default_output_template.md) - Summary Prompt: [Default Prompt](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_instructions_prompt.md) - File Content Strategy: Full file content - Correlation ID: `8ac5e9d0-97bf-11f1-9e0b-c00ff05b9b1b` - LLM: `anthropic--claude-4.6-sonnet` - Event Trigger: `pull_request.opened`
--- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c7791f4..c24be2d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,6 @@ jobs: node-version: 24 registry-url: https://registry.npmjs.org/ - name: run tests - # REVISIT: remove "npm explore better-sqlite3 -- npm run install" with cds^10 run: | npm i -g @sap/cds-dk npm i From 0b6211ec08c88a958e94a144b9cb2dfd0786ecc0 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:41:21 +0200 Subject: [PATCH 12/17] fix(logging): guard cds.log.format interception against re-entrancy; unpin sdk-logs (#482) (#489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Bumping the OTLP dev-deps to the `0.221` line pulled `@opentelemetry/sdk-logs` 0.219 → 0.221, which made `test/logging.test.js` OOM-crash (heap climbed to ~3.8 GB, ~110 s of GC thrashing before dying). #483 shipped an interim workaround pinning sdk-logs to `0.219.0` via a top-level `overrides` block. This PR removes that pin and fixes the actual defect. Closes #482. ## Root cause Two things combine: 1. **Constructor signature change (the real trigger).** In sdk-logs 0.221 the `SimpleLogRecordProcessor` and `BatchLogRecordProcessor` constructors changed from positional `(exporter)` to an options object `({ exporter })`. `lib/logging/index.js` still passed the exporter positionally (both the built-in path and the custom-processor path in `_getCustomProcessor`; the test's `MySimpleLogRecordProcessor` extends the SDK base and forwards its args). So `options.exporter` was `undefined`, and every emit called `core.internal._export(undefined, …)`, which throws. 2. **Diag re-entrancy loop.** The thrown export error hits `.catch(globalErrorHandler)` → `diag.error(...)`. Because `lib/index.js` wires `diag.setLogger(cds.log('telemetry'), …)`, that diagnostic goes back through `cds.log('telemetry')` → the overridden `cds.log.format` → `logger.emit()` → export throws again → … an unbounded loop. Each hop is a **microtask** (`processTicksAndRejections`), so the recursion is asynchronous. ## Fix - Construct the log processors with the `{ exporter }` options object 0.221 expects (built-in + custom-processor paths). This stops the export from throwing, which removes the source of the diag error loop — the OOM is gone. - Add a re-entrancy guard (`let emitting`) around `logger.emit()` in the `cds.log.format` interception: while inside our own `emit()` we still run the original format work (log output unchanged) but skip re-emitting. This is defense-in-depth against any **synchronous** re-entry through the export/diag path; `try/finally` guarantees the flag resets even if `emit()` throws. ## Unpin - Removed the `@opentelemetry/sdk-logs` `overrides` pin; regenerated the lockfile. sdk-logs now floats to `0.221.0`, matching the other OTLP deps. This removes the #483 workaround pin. ## Verification - `vitest run test/logging.test.js` — passes in ~1.3 s (was ~3.8 GB / ~110 s OOM crash on 0.221). The `logging.test.js` OOM was the repro; ran it repeatedly, stable. - Full sqlite suite: 63 passed / 14 skipped, no regressions. - eslint `--max-warnings=0` clean; `oxfmt --check` clean. - Lockfile: no internal-registry URLs; `npm ci` reproduces cleanly. --- CHANGELOG.md | 1 + lib/logging/index.js | 43 ++++++++++++++++++++++++-------- package-lock.json | 58 +++++--------------------------------------- package.json | 3 --- 4 files changed, 40 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ba12a8..ed9bec78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed +- Logging no longer recurses through `@opentelemetry/sdk-logs` 0.221's export path: the log-processor construction now adapts to the installed sdk-logs version (0.221+ takes an `{ exporter }` options object, earlier versions the positional exporter), and a re-entrancy guard was added to the `cds.log.format` interception - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) - Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) - Queue `*_storage_time_in_seconds` metrics are now correct on HANA (timezone-naive `min`/`max` timestamp aggregates were parsed as local time, skewing the values by the machine's UTC offset) diff --git a/lib/logging/index.js b/lib/logging/index.js index 45a1ea36..a1fe08e0 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -11,6 +11,19 @@ const _protocol2module = { 'http/json': '@opentelemetry/exporter-logs-otlp-http' } +// @opentelemetry/sdk-logs 0.221 changed the log processor constructors (Simple/Batch, and hence +// any subclass) from a positional exporter argument `(exporter)` to an options object `({ exporter })`. +// Passing the wrong shape leaves the exporter undefined, so every export throws and — since our diag +// logger is wired to cds.log — recurses back through cds.log.format into an unbounded loop (see #482). +// To stay correct regardless of which sdk-logs a consumer's tree resolves, detect the installed +// version and build the constructor argument accordingly (no hard version floor needed). +function logProcessorArg(exporter) { + const version = require('@opentelemetry/sdk-logs/package.json').version + const [major, minor] = version.split('.').map(Number) + const usesOptionsObject = major > 0 || minor >= 221 + return usesOptionsObject ? { exporter } : exporter +} + function _getExporter() { let { kind, @@ -67,7 +80,7 @@ function _getCustomProcessor(exporter) { if (!loggingProcessorModule[loggingProcessor.class]) throw new Error(`Unknown logs processor "${loggingProcessor.class}" in module "${loggingProcessor.module}"`) - const processor = new loggingProcessorModule[loggingProcessor.class](exporter) + const processor = new loggingProcessorModule[loggingProcessor.class](logProcessorArg(exporter)) LOG._debug && LOG.debug('Using logs processor:', processor) return processor @@ -89,13 +102,18 @@ module.exports = resource => { const custom_fields = cds.env.log.cls_custom_fields || [] // intercept logs via format + // re-entrancy guard: while we are inside our own logger.emit(), skip re-emitting anything + // that emit() logs synchronously (e.g. via the export path or the SDK's diag logger, which + // is wired to cds.log('telemetry')). This prevents cds.log.format from recursing back into + // logger.emit() and looping through the logs SDK's export/diagnostic output (#482). + let emitting = false const { format: _format } = cds.log const format = (cds.log.format = function (module, level, ...args) { const res = _format.call(this, module, level, ...args) let log try { - log = res.length === 1 && res[0].startsWith?.('{"') && JSON.parse(res[0]) + log = !emitting && res.length === 1 && res[0].startsWith?.('{"') && JSON.parse(res[0]) } catch { // ignore } @@ -115,12 +133,17 @@ module.exports = resource => { log.msg = log.msg.replace(e.stack, e.stack?.split('\n')[0]) } for (const field of custom_fields) if (field in log) attributes[field] = log[field] - logger.emit({ - severityNumber: SeverityNumber[severity], - severityText: severity, - body: log.msg, - attributes - }) + emitting = true + try { + logger.emit({ + severityNumber: SeverityNumber[severity], + severityText: severity, + body: log.msg, + attributes + }) + } finally { + emitting = false + } } return res @@ -137,8 +160,8 @@ module.exports = resource => { const processor = _getCustomProcessor(exporter) || (process.env.NODE_ENV === 'production' - ? new BatchLogRecordProcessor(exporter) - : new SimpleLogRecordProcessor(exporter)) + ? new BatchLogRecordProcessor(logProcessorArg(exporter)) + : new SimpleLogRecordProcessor(logProcessorArg(exporter))) /* * either add processor as delegate in CALM... diff --git a/package-lock.json b/package-lock.json index 3d2554fc..620f8da8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -675,15 +675,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", - "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -693,52 +693,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", diff --git a/package.json b/package.json index 3c52a465..b3128b8f 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,6 @@ "oxfmt": "0.63.0", "vitest": "^4" }, - "overrides": { - "@opentelemetry/sdk-logs": "0.219.0" - }, "cds": { "requires": { "telemetry": { From 0c28b57d54d4718efd0052db4fb91d67d29cc8c1 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:47:12 +0200 Subject: [PATCH 13/17] test: convert remote/span-name tracing tests to in-memory span exporter; drop tracing-attributes profile (#478) (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Group 1 of #478. Converts the three tracing tests that still spied on `console.dir` to read structured `ReadableSpan` objects directly from the in-memory span exporter: - `test/tracing-remote-cloudsdk.test.js` - `test/tracing-remote-native.test.js` - `test/tracing-span-names.test.js` Each now uses `--profile tracing-in-memory` (which wires `MyInMemorySpanExporter` via `.cdsrc.json`) and reads over `captured` (the module-level array of `ReadableSpan`s) instead of the `console.dir` spy. `beforeEach(reset)` clears the buffer per test. All existing assertions are preserved verbatim — `instrumentationScope.name` filters (`@cap-js/telemetry`, `@opentelemetry/instrumentation-undici`), span names, and attributes (`code.function.name`, `db.query.text`, `sap.btp.destination`, the no-raw-SQL / no-URL-in-span-name checks). `captured` holds full ReadableSpans with `instrumentationScope`, so the filters just point at `captured`. No flush/poll was needed: the spans for these single request/DB ops appear synchronously in `captured` after the awaited call. In `tracing-span-names.test.js`, `data.reset()` is itself traced, so the buffer is cleared *after* reset (mirroring `tracing-attributes.test.js`). ## Why No more `console.dir` spying in the tracing tests. With all three files migrated, the `[tracing-attributes]` profile in `test/bookshop/.cdsrc.json` has no remaining consumers (verified: only these 3 used it; `tracing-attributes.test.js` despite its name already uses `tracing-in-memory`), so it is removed. This also resolves the deferred "rename tracing-attributes → tracing-console" note — the profile simply goes away. Test-only change. No lib change, no CHANGELOG. Refs #478 (group 1 only; group 2 done via #479, group 3 stays). --- test/bookshop/.cdsrc.json | 11 ----------- test/tracing-remote-cloudsdk.test.js | 12 ++++++++---- test/tracing-remote-native.test.js | 12 ++++++++---- test/tracing-span-names.test.js | 19 +++++++++++++------ 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 7b046bcf..842dfd24 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -75,17 +75,6 @@ } } }, - "[tracing-attributes]": { - "requires": { - "telemetry": { - "tracing": { - "exporter": { - "module": "@opentelemetry/sdk-trace-node" - } - } - } - } - }, "[tracing-in-memory]": { "requires": { "telemetry": { diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 4adfcede..16b5b79e 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -1,17 +1,21 @@ const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') + // Cloud SDK path: with @sap-cloud-sdk/http-client installed (as in the bookshop) and // cds.env.remote.native_fetch NOT set, CAP routes outbound remote calls through // getCloudSdk().executeHttpRequestWithOrigin(...). lib/tracing/cloud_sdk.js wraps that // export so the outbound call produces a @cap-js/telemetry CLIENT span carrying // the sap.btp.destination attribute. describe('tracing remote via cloud sdk', () => { - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) + beforeEach(reset) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') let server, port diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index f930a5d8..857c5f19 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -3,19 +3,23 @@ process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') + // Native fetch path: when cds.env.remote.native_fetch === true (or no cloud sdk is // installed), CAP routes outbound remote calls through native fetch, which is // instrumented by @opentelemetry/instrumentation-undici. The outbound span therefore // comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and // NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. describe('tracing remote via native fetch', () => { - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) + beforeEach(reset) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured let server, port diff --git a/test/tracing-span-names.test.js b/test/tracing-span-names.test.js index 8ff39d39..73a30903 100644 --- a/test/tracing-span-names.test.js +++ b/test/tracing-span-names.test.js @@ -1,14 +1,21 @@ const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') -describe('span names', () => { - beforeEach(data.reset) +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) +describe('span names', () => { + beforeEach(async () => { + // data.reset is itself heavily traced (it runs DELETEs + INSERTs for the seed data) — + // run it first, THEN clear the buffer so the test only sees its own spans. + await data.reset() + reset() + }) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured // Spans from our tracer only (excludes HTTP instrumentation spans) const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') From 13e2c7e70868265610247ef129cd7e67257c042b Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:58:48 +0200 Subject: [PATCH 14/17] test: enable HTTP instrumentation in tests; assert incoming SERVER spans + traceparent propagation (#475) (#491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Re-enables HTTP instrumentation in the test app (disabled by #474 to stay behavior-neutral under jest) and asserts the incoming-request tracing behavior it produces. - **Test-app config** (`test/bookshop/package.json`): removed `disableIncomingRequestInstrumentation` / `disableOutgoingRequestInstrumentation` so both default to on; kept `ignoreIncomingRequestHook`. Incoming HTTP requests now produce a SERVER span (the root of each request trace) and outgoing requests produce CLIENT spans. - **`GET with traceparent is traced`** (was `xtest`): asserts the incoming SERVER span adopts the W3C context from the `traceparent` header — trace id `0af7651916cd43dd8448eb211c80319c`, parent span id `b7ad6b7169203331`. - **`instrumentation hooks`** (was empty `xtest`): asserts both suppression mechanisms — the sampler's `ignoreIncomingPaths` (`/odata/v4/admin/Authors`) and `MyIgnoreIncomingRequestHook` (`/Books(252)`) — produce NO incoming SERVER span, while a non-ignored path (`/Books`) does. - **`$batch is traced`**: updated for reparenting — the single `$batch` POST now yields ONE incoming SERVER root with both batch sub-operations (CREATE Genres draft, READ Genres) nested beneath it (was 2 roots). - **tracing-messaging CHECKs** (`without-outbox`, `inboxed`, `persistent-outbox`): the producer trace is now rooted at the incoming SERVER span (`AdminService - tx` nests under it), so the root assertion was updated from name `AdminService - tx` to `SpanKind.SERVER` while keeping the same nested-span assertions. ## Why The test's HTTP client runs in-process; with outgoing instrumentation now on, it would itself create a CLIENT span (an artificial extra root that also overwrites a manually-set `traceparent`). A shared `asExternalClient` helper runs client-side requests under `suppressTracing` to model a real external, un-instrumented caller — the outgoing CLIENT artifact is skipped and the genuine incoming SERVER span is the trace root. No `lib/` change (config + tests only). No CHANGELOG. ## Verification (sqlite) - `test/tracing.test.js` — 11 passed / 2 skipped (the 2 formerly-`xtest` now real + passing; `$batch` fixed; #477 §3 stubs remain skipped) - `test/tracing-remote-cloudsdk.test.js` + `test/tracing-remote-native.test.js` — pass (outgoing CLIENT instrumentation didn't break them) - Full suite — 65 passed / 12 skipped (was 63 / 14; the 2 newly-enabled tests moved skipped→passed) - lint + oxfmt clean; lockfile untouched ## HANA note `inboxed` and `persistent-outbox` messaging tests are HANA-only (skipped on sqlite) but share the same reparented producer-root assertion, so they were updated too — HANA CI must be verified on this PR. closes #475 --- test/bookshop/package.json | 4 +- test/tracing-messaging-inboxed.test.js | 11 ++- ...racing-messaging-persistent-outbox.test.js | 19 ++-- test/tracing-messaging-without-outbox.test.js | 17 +++- test/tracing-messaging.js | 10 +- test/tracing-scheduled.test.js | 24 +++-- test/tracing.test.js | 92 +++++++++++++++---- 7 files changed, 134 insertions(+), 43 deletions(-) diff --git a/test/bookshop/package.json b/test/bookshop/package.json index 7cabf2be..bf574fe9 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -22,9 +22,7 @@ "instrumentations": { "http": { "config": { - "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js", - "disableIncomingRequestInstrumentation": true, - "disableOutgoingRequestInstrumentation": true + "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js" } } }, diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index bd5fdb3d..d7a158fc 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -1,14 +1,18 @@ const CASE = 'inboxed' +const otel = require('@opentelemetry/api') + // `inboxed: true` combined with the default outboxed messaging behavior means TWO queue // workers get involved per emit — one on the producer side (drains outbox to broker) and // one on the consumer side (drains inbox to subscribers). Each worker runs two // transactions (tx 1: lock; tx 2: handle + delete). // // Each worker iteration is wrapped by `cds.spawn`, so both txs collapse under a single -// `cds.spawn - run task` root. 4 meaningful roots: +// `cds.spawn - run task` root. With incoming HTTP instrumentation on, the producer trace is +// rooted at the incoming SERVER span for the emit-triggering POST (AdminService - tx nests +// under it). 4 meaningful roots: // -// 1. AdminService - tx (producer: handle test_emit, UPSERT outbox) +// 1. POST (incoming SERVER span) (producer: AdminService - tx, handle test_emit, UPSERT outbox) // 2. cds.spawn - run task (outbox worker: dispatches to file) // ├─ db - tx (tx 1: lock) // └─ messaging - tx (tx 2: handle foo — writes to file — + DELETE) @@ -31,7 +35,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true // The inbox worker must have run the application handler (SELECT Books). diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 708e8802..6744e530 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -1,5 +1,7 @@ const CASE = 'persistent-outbox' +const otel = require('@opentelemetry/api') + // REVISIT: even with profile "persistent-outbox", messaging kind and file from package.json wins process.env.cds_requires_messaging = JSON.stringify({ kind: 'file-based-messaging', @@ -18,13 +20,17 @@ process.env.cds_requires_messaging = JSON.stringify({ // wraps to emit a single `cds.spawn - run task` CONSUMER root that both worker tx spans // nest under. // +// With incoming HTTP instrumentation on, the producer trace is rooted at the incoming SERVER +// span for the emit-triggering POST (AdminService - tx nests under it). +// // Expected shape (3 meaningful roots, same for sqlite and HANA): // -// 1. AdminService - tx (producer trace) -// └─ AdminService - handle test_emit -// └─ messaging - emit outgoing foo -// └─ db - UPSERT cds.outbox.Messages -// └─ cds.spawn - schedule task +// 1. POST (incoming SERVER span) (producer trace) +// └─ AdminService - tx +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task // // 2. cds.spawn - run task (queue worker root) // ├─ db - tx (tx 1) @@ -44,7 +50,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index 81138218..021a84a4 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -1,5 +1,7 @@ const CASE = 'without-outbox' +const otel = require('@opentelemetry/api') + // REVISIT: even with profile "without-outbox", messaging kind and file from package.json wins process.env.cds_requires_messaging = JSON.stringify({ kind: 'file-based-messaging', @@ -11,11 +13,15 @@ process.env.cds_requires_messaging = JSON.stringify({ // transaction (no queue worker). The file watcher delivers asynchronously as a new // SpanKind.CONSUMER root. // +// With incoming HTTP instrumentation on, the producer trace is now rooted at the incoming +// SERVER span (SpanKind.SERVER) for the emit-triggering POST; `AdminService - tx` nests under it. +// // Expected roots: -// 1. AdminService - tx (producer) -// └─ AdminService - handle test_emit -// └─ messaging - emit outgoing foo -// └─ messaging - handle foo (writes to file, in-process) +// 1. POST (incoming SERVER span) (producer) +// └─ AdminService - tx +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo (writes to file, in-process) // // 2. messaging - tx (file-based CONSUMER) // └─ messaging - emit outgoing foo @@ -29,7 +35,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name.match(/messaging - emit outgoing/))).to.be.true // File-based CONSUMER trace diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 147fc594..fd7cca22 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -3,6 +3,14 @@ module.exports = (CASE, CHECK) => { const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') + const { suppressTracing } = require('@opentelemetry/core') + + // The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, + // would itself create a CLIENT span for the emit-triggering POST — an artificial extra root + // above the incoming SERVER span. Real callers are separate, un-instrumented processes, so we + // run the request under suppressTracing to model that: the outgoing CLIENT span is skipped and + // the incoming SERVER span is the producer trace's root. + const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) const wait = require('node:timers/promises').setTimeout @@ -99,7 +107,7 @@ module.exports = (CASE, CHECK) => { }) test('emit is traced', async () => { - await POST('/odata/v4/admin/test_emit', {}, admin) + await asExternalClient(() => POST('/odata/v4/admin/test_emit', {}, admin)) // Poll (flush + re-check) until both queue workers have run and exported their spans; // on HANA the worker latency exceeds any reasonable fixed sleep. Pass the meaningful // (non-outbox-scan) traces so the CHECK's exact root-count assertions aren't thrown off by diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index aa9c2a49..d0c8b511 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -6,10 +6,11 @@ // // Expected meaningful roots (unified across sqlite and HANA): // -// 1. AdminService - tx (producer trace) -// └─ AdminService - handle test_scheduled -// └─ db - UPSERT cds.outbox.Messages -// └─ cds.spawn - schedule task +// 1. POST (incoming SERVER span) (producer trace) +// └─ AdminService - tx +// └─ AdminService - handle test_scheduled +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task // // 2. cds.spawn - run task (queue worker root) // ├─ db - tx (tx 1: lock) @@ -22,9 +23,17 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') const wait = require('node:timers/promises').setTimeout +// With incoming HTTP instrumentation on, the in-process test client would otherwise emit its own +// outgoing CLIENT span for the POST — an artifact that pollutes the producer trace and can even be +// picked as its root. Real callers are separate, un-instrumented processes, so run client requests +// under suppressTracing to model that: the CLIENT span is skipped and the genuine incoming SERVER +// span is the producer trace's root. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + // Force-flush the tracer provider's span processor so any spans buffered by background // queue/worker activity are exported into `captured`. The global provider is a // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; @@ -83,7 +92,7 @@ describe('tracing for scheduled tasks', () => { }) test('schedule .after() is fully traced through the queue worker', async () => { - await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) + await asExternalClient(() => POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } })) // Poll (flush + re-check) until the scheduled task has fired and all spans have been // exported; on HANA the worker latency exceeds any reasonable fixed sleep. @@ -91,7 +100,10 @@ describe('tracing for scheduled tasks', () => { // Producer trace: writes the task row inside the HTTP request tx. const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + // With incoming HTTP instrumentation on, the producer trace roots at the incoming SERVER + // span for the POST; `AdminService - tx` now nests under it. + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true diff --git a/test/tracing.test.js b/test/tracing.test.js index d81806c9..94138349 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -11,6 +11,14 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // console spying, no string-regex matching of formatted output. const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') + +// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, +// would itself create a CLIENT span for every request — an artificial extra root that also +// overwrites any manually-set `traceparent` header. Real callers are separate, un-instrumented +// processes, so we run client-side requests under suppressTracing to model that: the outgoing +// CLIENT span is skipped, the incoming SERVER span is created normally by the server handler. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) const wait = require('node:timers/promises').setTimeout @@ -67,12 +75,22 @@ describe('tracing', () => { expect(rootSpans().length).to.be.gte(1) }) - // REVISIT: jest breaks otel's patching of incoming request handling -> no span for 'GET' -> behavior to test not reproducible - xtest('GET with traceparent is traced', async () => { - const config = { ...admin, headers: { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' } } - const { status } = await GET('/odata/v4/admin/Books', config) + // With incoming HTTP instrumentation on, the SERVER span adopts the W3C trace context from the + // request's `traceparent` header: the whole request trace continues the given trace id and the + // SERVER span is a child of the given (external) span id. + test('GET with traceparent is traced', async () => { + const traceId = '0af7651916cd43dd8448eb211c80319c' + const parentSpanId = 'b7ad6b7169203331' + const config = { ...admin, headers: { traceparent: `00-${traceId}-${parentSpanId}-01` } } + const { status } = await asExternalClient(() => GET('/odata/v4/admin/Books', config)) expect(status).to.equal(200) expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true + // The incoming SERVER span continued the propagated trace and parented off the external span id. + await eventually(() => { + const server = captured.find(s => s.kind === otel.SpanKind.SERVER && s.spanContext().traceId === traceId) + expect(server, 'incoming SERVER span adopting the propagated trace').to.exist + expect(server.parentSpanContext?.spanId).to.equal(parentSpanId) + }) }) test('custom GET is traced', async () => { @@ -97,24 +115,60 @@ describe('tracing', () => { }) }) - // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible - xtest('instrumentation hooks', async () => {}) + // Incoming HTTP instrumentation produces a SERVER span (SpanKind.SERVER === 1) per request, + // carrying the mount-relative `url.path`. Two independent mechanisms suppress that span: + // - the sampler's `ignoreIncomingPaths` (set at the top of this file for /odata/v4/admin/Authors) + // - the `ignoreIncomingRequestHook` (MyIgnoreIncomingRequestHook: /odata/v4/admin/Authors + /Books(252)) + // A non-ignored path still produces a SERVER span; the ignored paths must produce none. + test('instrumentation hooks', async () => { + const serverSpansFor = path => + captured.filter(s => s.kind === otel.SpanKind.SERVER && s.attributes['url.path'] === path) + + // Baseline: a non-ignored path DOES yield an incoming SERVER span. + await asExternalClient(() => GET('/odata/v4/admin/Books', admin)) + await eventually(() => expect(serverSpansFor('/Books')).to.have.lengthOf(1)) + + // Sampler path: /odata/v4/admin/Authors is in ignoreIncomingPaths -> no SERVER span. + reset() + await asExternalClient(() => GET('/odata/v4/admin/Authors?$select=ID', admin)) + await eventually(() => { + expect(captured.some(s => s.kind === otel.SpanKind.SERVER && s.attributes['url.path']?.includes('/Authors'))).to + .be.false + }) + + // ignoreIncomingRequestHook path: /Books(252) is ignored by the hook (not the sampler) -> no SERVER span. + reset() + await asExternalClient(() => GET('/odata/v4/admin/Books(252)', admin)) + await eventually(() => { + expect(serverSpansFor('/Books(252)')).to.have.lengthOf(0) + }) + }) test('$batch is traced', async () => { - await POST( - '/odata/v4/genre/$batch', - { - requests: [ - { id: 'r1', method: 'POST', url: '/Genres', headers: { 'content-type': 'application/json' }, body: {} }, - { id: 'r2', method: 'GET', url: '/Genres', headers: {} } - ] - }, - admin + await asExternalClient(() => + POST( + '/odata/v4/genre/$batch', + { + requests: [ + { id: 'r1', method: 'POST', url: '/Genres', headers: { 'content-type': 'application/json' }, body: {} }, + { id: 'r2', method: 'GET', url: '/Genres', headers: {} } + ] + }, + admin + ) ) - // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — - // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + - // read drafts) are now nested under 2 root tx spans, one per batch entry. - await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(2)) + // With incoming HTTP instrumentation on, the single $batch POST produces one incoming SERVER + // span that becomes the trace root. Both batch sub-requests (the POST -> CREATE Genres draft + // and the GET -> READ Genres) run within that request context, so their tx spans reparent + // under the SERVER span rather than surfacing as separate roots. Result: exactly 1 meaningful + // root (the SERVER span), containing both the CREATE and the READ sub-operations. + await eventually(() => { + const roots = meaningfulRoots() + expect(roots).to.have.lengthOf(1) + expect(roots[0].kind).to.equal(otel.SpanKind.SERVER) + expect(captured.some(s => s.name === 'GenreService - CREATE GenreService.Genres.drafts')).to.be.true + expect(captured.some(s => s.name === 'GenreService - READ GenreService.Genres')).to.be.true + }) }) test('cds.spawn is traced', async () => { From 65157bcd521c556c7db9b79080d8bab0384f2639 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:08:49 +0200 Subject: [PATCH 15/17] test: centralize duplicated test helpers into test-utils (#488) (#492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Extracts the six test helpers that had been copy-pasted across ~10 test files into one shared module, `test/bookshop/lib/test-utils.js`: - `flushSpans()` — unwrap the ProxyTracerProvider → delegate and `forceFlush()`. - `eventually(fn, { flush, timeout, interval })` — the unified state-based poll. The span `eventually` and the metric `expectEventually` were structurally identical, differing only in the flush target + defaults, so they are now one function. `flush` defaults to `flushSpans` (span callers); metric callers pass the reader's `forceFlush` and their own timeout/interval. - `clearOutbox(timeout = 5000)` — timeout-bounded `DELETE FROM cds.outbox.Messages`. - `asExternalClient(fn)` — runs `fn` under `suppressTracing`. - `isOutboxScanTrace(g)` / `meaningful(groups)` — the outbox-scan-trace filter, reconciled to a single `meaningful(groups)` signature. ## Why These helpers landed independently during the HANA / instrumentation work and drifted into ~10 near-identical copies. Centralizing them removes the duplication (net -141 lines) and gives one place to maintain the polling / flush / outbox-scan-filter logic. ## Notes - **Behavior-preserving refactor** — no assertion changes, no timeout/interval drift (each call site passes through its original values), no predicate changes. - Test-only. No `lib/**` change, no CHANGELOG (test-only), no dependency change. - `test-utils.js` stays dependency-light: it only requires `@opentelemetry/api`, `@opentelemetry/core`, and `node:timers/promises` — it does **not** `require('@sap/cds')` at module top (same rule that protects the sibling exporters/reader). `clearOutbox` uses the global `DELETE` provided by `cds.test()` at call time. ## Verification - Full sqlite suite: **65 passed / 12 skipped** — matches the develop baseline exactly. - Per-file runs (metrics-outbox, metrics-outbox-multitenant, metrics, tracing, tracing-messaging-without-outbox) all pass; the sqlite-gated tracing files (outboxed-batch, scheduled) skip as before. - eslint `--max-warnings=0` clean, `oxfmt --check` clean. ## HANA CI Several affected files run **only on HANA** and share these helpers, so they cannot be validated locally — please confirm HANA CI is green for: - `test/tracing-scheduled.test.js` - `test/tracing-messaging-inboxed.test.js` - `test/tracing-messaging-persistent-outbox.test.js` - `test/tracing-outboxed-batch.test.js` (`test/tracing-messaging-without-outbox.test.js` does run on sqlite and exercises the shared `meaningful`/`eventually` path — confirmed passing.) closes #488 --- test/metrics-outbox-multitenant.test.js | 27 ++----- test/metrics-outbox.test.js | 41 ++-------- test/metrics.test.js | 26 ++----- test/tracing-messaging.js | 63 +--------------- test/tracing-outboxed-batch.test.js | 33 +-------- test/tracing-scheduled.test.js | 40 +--------- test/tracing.test.js | 52 +------------ test/utils.js | 99 +++++++++++++++++++++++++ 8 files changed, 125 insertions(+), 256 deletions(-) create mode 100644 test/utils.js diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index e5115d18..3f7956bf 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -4,6 +4,7 @@ const { setTimeout: wait } = require('node:timers/promises') // Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the // metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { makeExpectEventually } = require('./utils') const { expect, GET, axios } = cds.test( __dirname + '/bookshop', @@ -17,26 +18,12 @@ function metricValue(tenant, metric) { return latestDataPointValue(metric, { 'sap.tenancy.tenant_id': tenant }) } -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces all fixed-time `wait(…)` sleeps — the loop completes the instant the in-memory -// per-tenant queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted -// state. forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails -// loudly instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces all fixed-time `wait(…)` sleeps — the loop +// completes the instant the in-memory per-tenant queue statistics (kept fresh by the existing +// cds.spawn poller) reflect the asserted state. forceFlush() throws fast if the provider isn't +// wired, so a misconfigured profile fails loudly instead of busy-spinning the full timeout. +const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) // Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. // The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index dc079f6d..ce1847f2 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -9,6 +9,7 @@ const { setTimeout: wait } = require('node:timers/promises') // Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the // metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { clearOutbox, makeExpectEventually } = require('./utils') const { expect, GET, axios } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox') axios.defaults.validateStatus = () => true @@ -17,25 +18,11 @@ function metricValue(metric, queuedServiceName) { return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) } -// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI -// container a background queue worker may be holding the connection pool (draining/retrying), -// so a bare `DELETE` can block indefinitely — which previously turned into a 100s hook timeout -// that starved the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the -// DELETE against a short timeout and swallow errors: if it can't complete quickly, the leftover -// rows are handled by the next file's own beforeEach clear anyway. -async function clearOutbox(timeout = 5000) { - try { - await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) - } catch { - // pool draining / server shutting down — nothing left to clean matters - } -} - -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory -// queue statistics (kept fresh by the queue-stats cds.spawn poller) reflect the asserted state. -// forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly -// instead of busy-spinning the full timeout. +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces all fixed-time `wait(150)` sleeps — the loop +// completes the instant the in-memory queue statistics (kept fresh by the queue-stats cds.spawn +// poller) reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a +// misconfigured profile fails loudly instead of busy-spinning the full timeout. // // interval is 500ms (NOT a few ms): each forceFlush() triggers a metric collection that runs the // queue-stats poller's SELECTs against the DB. On the SHARED HANA HDI container a tight poll loop @@ -45,21 +32,7 @@ async function clearOutbox(timeout = 5000) { // Polling at 500ms (with the profile's exportIntervalMillis raised to 1000ms) leaves the worker // enough DB headroom to make all its attempts. The loop still returns the instant the state holds, // so sqlite (per-file in-memory DB) still satisfies in well under a second. -async function expectEventually(assertion, { timeout = 30000, interval = 500 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const expectEventually = makeExpectEventually(forceFlush, { timeout: 30000, interval: 500 }) const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) diff --git a/test/metrics.test.js b/test/metrics.test.js index 6582b7a3..84d68f1d 100644 --- a/test/metrics.test.js +++ b/test/metrics.test.js @@ -4,31 +4,17 @@ // log output. The formatting of those metrics is unit-tested in console-metric-exporter.test.js. const cds = require('@sap/cds') -const { setTimeout: wait } = require('node:timers/promises') const { captured, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { makeExpectEventually } = require('./utils') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'metrics') -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces fixed-time sleeps — the loop completes the instant the captured datapoints -// reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a -// misconfigured profile fails loudly instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces fixed-time sleeps — the loop completes the +// instant the captured datapoints reflect the asserted state. forceFlush() throws fast if the +// provider isn't wired, so a misconfigured profile fails loudly instead of busy-spinning the timeout. +const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) // All metric descriptor names present across every captured export. function capturedMetricNames() { diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index fd7cca22..ef12a8a0 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -2,73 +2,12 @@ module.exports = (CASE, CHECK) => { const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') - const otel = require('@opentelemetry/api') - const { suppressTracing } = require('@opentelemetry/core') - - // The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, - // would itself create a CLIENT span for the emit-triggering POST — an artificial extra root - // above the incoming SERVER span. Real callers are separate, un-instrumented processes, so we - // run the request under suppressTracing to model that: the outgoing CLIENT span is skipped and - // the incoming SERVER span is the producer trace's root. - const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + const { asExternalClient, clearOutbox, eventually, meaningful } = require('./utils') const wait = require('node:timers/promises').setTimeout - // Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI - // container a background queue worker may hold the connection pool, so a bare DELETE can block - // indefinitely — which would turn into a hook timeout that starves the pool and cascades into - // ECONNREFUSED for the next file's server. Race the DELETE against a short timeout; leftover - // rows are cleared by the next file's own beforeEach anyway. - async function clearOutbox(timeout = 5000) { - try { - await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) - } catch { - // pool draining during shutdown — nothing left to clean matters - } - } - - // Force-flush the tracer provider's span processor so any spans buffered by background - // queue-worker activity are exported into `captured`. The global provider is a - // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; - // guard for the no-op provider so a misconfigured profile fails loudly, not silently. - async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() - } - - // State-based wait: repeatedly flush + re-run the assertion until it holds or times out. - // Replaces the fixed `wait(waitMs)` sleep that flakes on HANA, where the two queue workers - // flush their spans well after any reasonable fixed window. - async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } - } - const admin = { auth: { username: 'alice' } } - // The queue scheduler periodically scans `cds.outbox.Messages` in its own `db - tx` (a - // SELECT + optional UPDATE that finds nothing to dispatch). On HANA these bookkeeping scans - // land as extra root traces that have nothing to do with the emit under test — and because - // the single HDI container is shared across all test files, scans triggered by other files' - // lingering workers show up too. Filter those pure outbox-scan traces so the CHECKs' exact - // root-count assertions stay stable. A scan trace is a `db - tx` root whose every span only - // touches `cds.outbox.Messages` (no application entity, no messaging/handle span). - const isOutboxScanTrace = g => - g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) - const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) - const rm = () => { try { require('fs').rmSync(require('path').join(__dirname, CASE)) diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index a1d8f9bc..a7a4d44c 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -6,38 +6,7 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') const { hrTimeToNanoseconds } = require('@opentelemetry/core') -const otel = require('@opentelemetry/api') - -const wait = require('node:timers/promises').setTimeout - -// Force-flush the tracer provider's span processor so any spans buffered by background -// outbox/queue activity are exported into `captured`. The global provider is a -// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; -// guard for the no-op provider so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where background work flushes spans -// after the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const { eventually } = require('./utils') describe('tracing for outboxed batch (chunk-size fan-out)', () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index d0c8b511..31c33182 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -23,45 +23,7 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') -const { suppressTracing } = require('@opentelemetry/core') - -const wait = require('node:timers/promises').setTimeout - -// With incoming HTTP instrumentation on, the in-process test client would otherwise emit its own -// outgoing CLIENT span for the POST — an artifact that pollutes the producer trace and can even be -// picked as its root. Real callers are separate, un-instrumented processes, so run client requests -// under suppressTracing to model that: the CLIENT span is skipped and the genuine incoming SERVER -// span is the producer trace's root. -const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) - -// Force-flush the tracer provider's span processor so any spans buffered by background -// queue/worker activity are exported into `captured`. The global provider is a -// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; -// guard for the no-op provider so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where the worker flushes spans after -// the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const { asExternalClient, eventually } = require('./utils') describe('tracing for scheduled tasks', () => { // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite diff --git a/test/tracing.test.js b/test/tracing.test.js index 94138349..4e7508b0 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -11,55 +11,9 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // console spying, no string-regex matching of formatted output. const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') -const { suppressTracing } = require('@opentelemetry/core') - -// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, -// would itself create a CLIENT span for every request — an artificial extra root that also -// overwrites any manually-set `traceparent` header. Real callers are separate, un-instrumented -// processes, so we run client-side requests under suppressTracing to model that: the outgoing -// CLIENT span is skipped, the incoming SERVER span is created normally by the server handler. -const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) - -const wait = require('node:timers/promises').setTimeout - -// Force-flush the tracer provider's span processor so any spans buffered by background -// activity are exported into `captured`. The global provider is a ProxyTracerProvider (no -// forceFlush) whose delegate is the real NodeTracerProvider; guard for the no-op provider -// so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where spawned/emitted work flushes -// spans after the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} - -// On HANA the persistent-outbox queue poller periodically scans `cds.outbox.Messages` in its -// own `db - tx`, producing an extra root trace that is unrelated to what these tests exercise. -// Filter those bookkeeping traces out so root-count assertions stay stable across both DBs. -const isOutboxScanTrace = g => - g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) -const meaningfulRoots = () => - groupedByTrace() - .filter(g => !isOutboxScanTrace(g)) - .flatMap(g => g.roots) +const { asExternalClient, eventually, meaningful } = require('./utils') + +const meaningfulRoots = () => meaningful(groupedByTrace()).flatMap(g => g.roots) describe('tracing', () => { const admin = { auth: { username: 'alice' } } diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 00000000..66400b5f --- /dev/null +++ b/test/utils.js @@ -0,0 +1,99 @@ +// Shared test helpers, centralized here so the ~10 tracing/metrics suites stop copy-pasting them. +// +// Kept dependency-light on purpose: it does NOT `require('@sap/cds')` at module top (doing so once +// broke span capture for the in-memory span exporter — the cds require has to happen inside the test +// file, after the profile is applied). Only @opentelemetry primitives + node timers here. +// +// `clearOutbox` uses the global `DELETE` (the cds query API). That global is installed by cds.test() +// in the test process, and clearOutbox is only ever called from within hooks/tests (after setup), so +// the global is reliably present at call time — the module never needs to require @sap/cds itself. + +const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') +const { setTimeout: wait } = require('node:timers/promises') + +// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, would +// itself create a CLIENT span for every request — an artificial extra root that also overwrites any +// manually-set `traceparent` header. Real callers are separate, un-instrumented processes, so we run +// client-side requests under suppressTracing to model that: the outgoing CLIENT span is skipped and +// the incoming SERVER span is created normally by the server handler. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + +// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI +// container a background queue worker may be holding the connection pool (draining/retrying), so a +// bare `DELETE` can block indefinitely — which previously turned into a hook timeout that starved +// the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the DELETE against a +// short timeout and swallow errors: if it can't complete quickly, the leftover rows are handled by +// the next file's own beforeEach clear anyway. +async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining / server shutting down — nothing left to clean matters + } +} + +// Force-flush the tracer provider's span processor so any spans buffered by background activity are +// exported into `captured`. The global provider is a ProxyTracerProvider (no forceFlush) whose +// delegate is the real NodeTracerProvider; guard for the no-op provider so a misconfigured profile +// fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. Replaces +// the fixed `wait(...)` sleeps that flake on HANA, where background/spawned work flushes its data +// after any reasonable fixed window. +// +// `flush` is the target to force before each re-check. It defaults to `flushSpans` (the span +// callers). Metric callers pass the meter provider's `forceFlush` (exported by MyInMemoryMetricReader) +// — passing it keeps this module from depending on the reader. Defaults (timeout 15000, interval 50) +// match the span call sites; metric call sites pass their own timeout/interval explicitly. +async function eventually(fn, { flush = flushSpans, timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flush() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// Build an `expectEventually(assertion)` bound to a specific flush target + poll defaults, so the +// metric suites don't each re-declare the same one-line wrapper. Metric callers pass the meter +// provider's `forceFlush` and their own {timeout, interval} (which vary per suite); the returned +// helper takes just the assertion. Equivalent to `a => eventually(a, { flush, timeout, interval })`. +const makeExpectEventually = + (flush, { timeout, interval } = {}) => + assertion => + eventually(assertion, { flush, timeout, interval }) + +// On HANA the persistent-outbox queue scheduler periodically scans `cds.outbox.Messages` in its own +// `db - tx` (a SELECT + optional UPDATE that finds nothing to dispatch). Those land as extra root +// traces unrelated to the emit under test — and because the single HDI container is shared across all +// test files, scans triggered by other files' lingering workers show up too. Filter those pure +// outbox-scan traces so the exact root-count assertions stay stable. A scan trace is a `db - tx` root +// whose every span only touches `cds.outbox.Messages` (no application entity, no messaging/handle span). +const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) + +// Drop the outbox-scan bookkeeping traces from a `groupedByTrace()` array, returning the meaningful groups. +const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) + +module.exports = { + asExternalClient, + clearOutbox, + flushSpans, + eventually, + makeExpectEventually, + isOutboxScanTrace, + meaningful +} From f125e34bf9eccac67569037c3a62b3229d60f63c Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:50:40 +0200 Subject: [PATCH 16/17] test: configure via .cdsrc.json profiles instead of process.env.cds_* (#486) (#493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Replaces the load-order-fragile `process.env.cds_requires_*` / `process.env.cds_*` string-JSON test config with proper cds config: `.cdsrc.json` profiles (composed with `--profile`) and `cds.test()` args. Test-only + test-app-config change; no `lib/` change. ## Why Setting config via `process.env.cds_*` string-JSON at module top is load-order-sensitive: it must run before any `@sap/cds` require or it is silently ignored (the root of the removed `delete cds.env` hack). cds already supports the same config declaratively via `.cdsrc.json` profiles. Root cause of the old `// REVISIT: ... package.json wins` comments (why some config had to be done via env): cds loads config sources sequentially, `package.json` **after** `.cdsrc.json`, last-writer-wins. So a `.cdsrc.json` profile could never override a key that `package.json` set at its base. Fix: move those base defaults (`log.cls_custom_fields`, `messaging.kind`/`file`) from `test/bookshop/package.json` into `test/bookshop/.cdsrc.json` base, so the `.cdsrc.json` profiles (same source) win as intended. ## Sites moved to profiles (all 8 — zero `process.env.cds_*` remain) | Site | Was | Now | |---|---|---| | `logging.test.js` (cls_custom_fields, tracing.exporter:false) | `cds_log`, `cds_requires_telemetry_tracing` | `[logging]` profile (+ base moved out of package.json) | | `tracing.test.js` (sampler ignoreIncomingPaths) | `cds_requires_telemetry_tracing_sampler` | new `[sampler-ignore-authors]` profile, `--profile 'tracing-in-memory, sampler-ignore-authors'` | | `tracing-remote-native.test.js`, `tracing-attributes.test.js` (native_fetch) | `cds_remote_native__fetch` | new `[native-fetch]` profile (`remote.native_fetch: true`), `--profile 'tracing-in-memory, native-fetch'` | | `passport.test.js` (scheduling off) | `cds_requires_scheduling` | new `[no-scheduling]` profile, `cds.test(dir, '--profile', 'no-scheduling')` | | `tracing-messaging-{inboxed,persistent-outbox,without-outbox}.test.js` (messaging kind/file/flags) | `cds_requires_messaging` | existing `[inboxed]`/`[persistent-outbox]`/`[without-outbox]` profiles now fully carry it (messaging base moved out of package.json) | ## .cdsrc.json changes - Added base `requires.messaging` (local-messaging / msg-box) and `log.cls_custom_fields` (moved from package.json). - `[logging]`: added `requires.telemetry.tracing.exporter: false`. - New profiles: `[sampler-ignore-authors]`, `[native-fetch]`, `[no-scheduling]`. ## Verification - Full sqlite suite: **65 passed / 12 skipped** (identical to baseline, same skip set). - Each converted file passes individually on sqlite. - `grep -rn "process.env.cds_" test/` → none remain. - eslint (test/) + oxfmt clean; `grep -c int.repositories.cloud.sap package-lock.json` = 0. ## HANA CI caveat Profile changes are DB-agnostic, but these run on HANA and could only be validated for config equivalence locally (config resolves identically to the old env), not executed: `logging` (runs on both), and the HANA-only `tracing-messaging-inboxed` + `tracing-messaging-persistent-outbox`. HANA CI must validate them. No lib change. No behavior change. Closes #486 --- test/bookshop/.cdsrc.json | 32 +++++++++++++++++++ test/bookshop/package.json | 10 ------ test/logging.test.js | 18 +++++------ test/passport.test.js | 8 ++--- test/tracing-attributes.test.js | 6 ++-- test/tracing-messaging-inboxed.test.js | 9 ++---- ...racing-messaging-persistent-outbox.test.js | 7 ++-- test/tracing-messaging-without-outbox.test.js | 8 ++--- test/tracing-remote-native.test.js | 9 +++--- test/tracing.test.js | 12 ++++--- 10 files changed, 65 insertions(+), 54 deletions(-) diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 842dfd24..4b9e60df 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -1,8 +1,19 @@ { + "requires": { + "messaging": { + "kind": "local-messaging", + "_kind": "file-based-messaging", + "file": "../msg-box" + } + }, + "log": { + "cls_custom_fields": ["tenant_id"] + }, "[logging]": { "requires": { "telemetry": { "tracing": { + "exporter": false, "sampler": { "ignoreIncomingPaths": ["/odata/v4/admin/Genres"] } @@ -87,6 +98,27 @@ } } }, + "[sampler-ignore-authors]": { + "requires": { + "telemetry": { + "tracing": { + "sampler": { + "ignoreIncomingPaths": ["/odata/v4/admin/Authors"] + } + } + } + } + }, + "[native-fetch]": { + "remote": { + "native_fetch": true + } + }, + "[no-scheduling]": { + "requires": { + "scheduling": false + } + }, "[persistent-outbox]": { "requires": { "messaging": { diff --git a/test/bookshop/package.json b/test/bookshop/package.json index bf574fe9..0c5c0c71 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -39,11 +39,6 @@ } } }, - "messaging": { - "kind": "local-messaging", - "_kind": "file-based-messaging", - "file": "../msg-box" - }, "queue": { "legacyLocking": false }, @@ -76,11 +71,6 @@ } } }, - "log": { - "cls_custom_fields": [ - "tenant_id" - ] - }, "fiori": { "draft_deletion_timeout": false } diff --git a/test/logging.test.js b/test/logging.test.js index 84f06f3b..c75b5000 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -1,14 +1,14 @@ /* eslint-disable no-console */ -// REVISIT: even with profile "logging", cls_custom_fields from package.json wins -process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) - -// This test asserts the exported LogRecords only. Disable the tracing signal (no exporter → -// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed -// times:" trace primer is never produced and can't land in the console.dir spy window. Without -// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. -process.env.cds_requires_telemetry_tracing = JSON.stringify({ exporter: false }) - +// Config lives in the `[logging]` profile of test/bookshop/.cdsrc.json: +// - log.cls_custom_fields: ['foo'] — the profile's own override (the base default is +// ['tenant_id']). #486 moved the base `cds.log`/`messaging` defaults out of package.json into +// .cdsrc.json base so profiles can win: package.json config loads AFTER .cdsrc.json and would +// otherwise override any profile. +// - requires.telemetry.tracing.exporter: false to disable the tracing signal (no exporter → +// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed +// times:" trace primer is never produced and can't land in the console.dir spy window. Without +// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') diff --git a/test/passport.test.js b/test/passport.test.js index 1f9de9b0..0b58693e 100644 --- a/test/passport.test.js +++ b/test/passport.test.js @@ -2,11 +2,11 @@ process.env.SAP_PASSPORT = 'true' // CDS v10 enables scheduling by default; its periodic outbox reads run in // their own transactions and cause spurious SAP_PASSPORT set/reset pairs on -// the connection, breaking the deterministic _count assertions below. -process.env.cds_requires_scheduling = 'false' - +// the connection, breaking the deterministic _count assertions below. The +// `no-scheduling` profile (requires.scheduling: false) in test/bookshop/.cdsrc.json +// disables it. const cds = require('@sap/cds') -const { expect, GET } = cds.test().in(__dirname + '/bookshop') +const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'no-scheduling') describe('SAP Passport', () => { if (cds.env.requires.db.kind === 'sqlite') return test.skip('n/a for SQLite', () => {}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 5b539128..535cf863 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -1,8 +1,8 @@ // Use native fetch in CDS OQ so @opentelemetry/instrumentation-undici can see outbound calls -process.env.cds_remote_native__fetch = 'true' - +// (cds.env.remote.native_fetch = true), configured via the `native-fetch` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory, native-fetch') const http = require('http') // The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index d7a158fc..399a335e 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -24,13 +24,8 @@ const otel = require('@opentelemetry/api') // // Tolerated: allow one extra root for the scheduling-service bookkeeping startup scan. -// REVISIT: profile config wins for kind/file, but explicit env override sidesteps it. -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}`, - inboxed: true -}) - +// Messaging config (kind/file/inboxed) comes from the `inboxed` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 6744e530..bb00d588 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -2,11 +2,8 @@ const CASE = 'persistent-outbox' const otel = require('@opentelemetry/api') -// REVISIT: even with profile "persistent-outbox", messaging kind and file from package.json wins -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}` -}) +// Messaging config (kind/file) comes from the `persistent-outbox` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. // --- Span hierarchy for the persistent-outbox case --------------------------------------- // diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index 021a84a4..15b9b647 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -2,12 +2,8 @@ const CASE = 'without-outbox' const otel = require('@opentelemetry/api') -// REVISIT: even with profile "without-outbox", messaging kind and file from package.json wins -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}`, - outboxed: false -}) +// Messaging config (kind/file/outboxed) comes from the `without-outbox` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. // Without outbox, file-based messaging writes directly to the file from the producer's // transaction (no queue worker). The file watcher delivers asynchronously as a new diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 857c5f19..9d6a4954 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -1,9 +1,8 @@ -// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk). -// This must be set before @sap/cds is loaded, so it lives at the very top of the file. -process.env.cds_remote_native__fetch = 'true' - +// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk) via the +// `native-fetch` profile (cds.env.remote.native_fetch = true) in test/bookshop/.cdsrc.json, +// composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory, native-fetch') const http = require('http') // The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures diff --git a/test/tracing.test.js b/test/tracing.test.js index 4e7508b0..47407588 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -1,10 +1,12 @@ // REVISIT: jest breaks otel's patching of incoming request handling -> we can't ignore via ignoreIncomingRequestHook -process.env.cds_requires_telemetry_tracing_sampler = JSON.stringify({ - ignoreIncomingPaths: ['/odata/v4/admin/Authors'] -}) - +// The sampler's ignoreIncomingPaths (/odata/v4/admin/Authors) is configured via the +// `sampler-ignore-authors` profile in test/bookshop/.cdsrc.json, composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect, GET, POST } = cds.test( + __dirname + '/bookshop', + '--profile', + 'tracing-in-memory, sampler-ignore-authors' +) // Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter // (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no From 057c829f6732ea1fb7d5739bbe02fb5be096e0db Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:22:10 +0200 Subject: [PATCH 17/17] docs: add TESTING.md; trim duplicated test comments to pointers (#487) (#494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Consolidates the hard-won test-suite knowledge that was scattered (and duplicated) across `test/**` comment blocks into a single, authoritative top-level **`TESTING.md`**, then trims the duplicated in-file comments down to short pointers. **New `TESTING.md`** covers: - Running the tests (`npm test` / vitest, sqlite default; CI matrix Node 22/24 × cds 9/10; where the bookshop app lives) - sqlite vs HANA (per-file in-memory DB vs one shared HDI container, and the implications: serial file execution, raised timeouts, `retry: 2`, outbox clear/settle; how the HANA path is signalled via `TELEMETRY_TEST_HANA`; HANA is a separate `workflow_dispatch`-only workflow, not PR CI) - Config via `.cdsrc.json` profiles (not env) — the profile table + compose syntax + the #486 load-order gotcha (don't reintroduce `process.env.cds_*`) - In-memory test infrastructure (`MyInMemorySpanExporter` / `MyInMemoryMetricReader`, DELTA temporality, the no-`require('@sap/cds')`-at-top rule) - Shared test helpers (`test/utils.js` from #488) and the flush+poll pattern - HTTP instrumentation (#475) and why client requests are wrapped in `asExternalClient` - The only two sanctioned skips (#477): SAP Passport on sqlite; multitenancy on HANA — plus the #477-tracked debt skips - Known caveats (the `startup > NO_TELEMETRY=true` local-env artifact; the internal-registry lockfile trap) **Trimmed duplicated comments to pointers** (comment-only, no logic change) in: `tracing-scheduled`, `tracing-outboxed-batch`, `tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox`, `tracing-messaging.js`, `tracing-mt`, `metrics-outbox-multitenant`. The repeated `cds.spawn on sqlite` skip rationale, the shared-HANA-container outbox-bleed explanation, and the multitenancy Service-Manager note now live in TESTING.md; each site keeps a one-line pointer. Genuinely local rationale (timezone-bug explanation, per-test tree shapes, `test/utils.js` own doc comments) is left untouched. **README** gets a one-line link to TESTING.md under the contributing section. ## Notes - No lib/behavior change. Test-file edits are **comment-only** (verified: every changed line in `test/**` starts with `//`). No change to test logic, assertions, config, `vitest.config.mjs`, `.cdsrc.json`, the exporters/reader, or the #477 gated skip logic. - Full sqlite suite unchanged: **65 passed / 12 skipped**. - `oxfmt --check` clean; changed files lint clean; `grep -c int.repositories.cloud.sap package-lock.json` = 0 (lockfile untouched). - No CHANGELOG entry (docs/test-only, not user-facing lib). closes #487 --- TESTING.md | 134 ++++++++++++++++++ test/metrics-outbox-multitenant.test.js | 5 +- test/tracing-messaging-inboxed.test.js | 7 +- ...racing-messaging-persistent-outbox.test.js | 7 +- test/tracing-messaging.js | 14 +- test/tracing-mt.test.js | 5 +- test/tracing-outboxed-batch.test.js | 13 +- test/tracing-scheduled.test.js | 16 +-- 8 files changed, 154 insertions(+), 47 deletions(-) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..07c99264 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,134 @@ +# Testing + +This document is the single, authoritative place for the hard-won, non-obvious knowledge behind the `@cap-js/telemetry` test suite. It is meant for contributors: read it before adding or debugging a test. Test files keep only the rationale that is local to a specific test; anything general or repeated lives here. + +## Running the tests + +```sh +npm test # vitest, sqlite in-memory (the default) +node_modules/.bin/vitest run # same, without the --silent from the npm script +``` + +- **Runner:** [Vitest](https://vitest.dev). Config in [`vitest.config.mjs`](vitest.config.mjs). +- **Default database:** `@cap-js/sqlite`, in-memory. No external services are needed for the default run. +- **Test app:** a small bookshop under [`test/bookshop`](test/bookshop) — CDS model, services, and test-only wiring (exporters/reader, ignore hooks). Each test spins it up with `cds.test(__dirname + '/bookshop', ...)`. +- **CI matrix:** Node 22 & 24 × cds 9 & 10 (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). The lint job additionally runs ESLint and `oxfmt --check`. + +Lint / format locally: + +```sh +npx eslint . --max-warnings=0 # npm run lint +npx oxfmt --check # npm run format:check +``` + +## sqlite vs HANA + +The suite runs on two databases, and the difference in DB **isolation model** drives most of the test infrastructure. + +| | sqlite (default / PR CI) | HANA (separate workflow) | +| --- | --- | --- | +| DB per test file | **Own** in-memory DB — each file is fully isolated | **One shared** HDI container across *all* files | +| File parallelism | Full parallelism | **Serial** (`fileParallelism: false`) | +| Timeouts | 42s test / 30s hook | **10×** test timeout | +| Retries | 0 (deterministic) | `retry: 2` (self-heal unlucky timing) | +| Outbox bleed | Impossible (fresh DB) | Must be actively prevented (see below) | + +Because HANA reuses one container for the whole run, a background queue/outbox worker from one file can still be draining when the next file starts and would dispatch leftover rows — adding foreign `cds.spawn - run task` root spans that break exact root-count assertions. The queue/outbox test files therefore: + +- **clear the outbox** in `beforeEach` (before resetting the span buffer, so the `DELETE`'s own spans aren't captured), and +- **settle** in `afterAll`: clear, wait for the last worker iteration, clear again — every clear timeout-bounded via `clearOutbox` so a draining pool can't hang the hook. + +All of this is a **no-op on sqlite** (fresh in-memory DB per file), gated on the HANA signal. + +### How the HANA path is signalled + +- The HANA job runs when `process.env.CI && process.env.HANA_DRIVER` are set. On that path `vitest.config.mjs` raises the timeout, disables file parallelism, enables retries, and excludes the multitenancy suites (see [Sanctioned skips](#sanctioned-skips)). +- It also sets **`process.env.TELEMETRY_TEST_HANA = '1'`** in the config module. Test files that must branch at **collection time** (before `cds.test()` applies its `--profile`) read this env var rather than `cds.env`: reading `cds.env` that early would freeze the env singleton before the profile is applied, so the tracer provider would be built with the wrong exporter and no spans would be captured. +- HANA runs from its **own workflow**, [`.github/workflows/hana.yml`](.github/workflows/hana.yml) — `workflow_dispatch` only, against a protected `hana` environment with a pre-provisioned HDI container. It is **not** part of the PR CI. + +## Configuration via profiles (not env) + +Test configuration lives in **[`test/bookshop/.cdsrc.json`](test/bookshop/.cdsrc.json)** as cds config profiles, selected per test file: + +```js +cds.test(dir, '--profile', 'tracing-in-memory') // one profile +cds.test(dir, '--profile', 'metrics-outbox, multitenancy') // profiles compose +``` + +| Profile | What it does | +| --- | --- | +| `[logging]` | Disables the tracing exporter (`false`) so no outbox-scan trace primer leaks into the console spy; wires a `ConsoleLogRecordExporter` + custom processor; sets `log.format: json` and `cls_custom_fields: ['foo']`. | +| `[metrics]` | Wires `MyInMemoryMetricReader`; short `exportIntervalMillis` (100). | +| `[metrics-outbox]` | Enables the queue (`_queue: true`) + in-memory reader; `exportIntervalMillis` 1000 (leaves the shared HANA worker DB headroom). | +| `[metrics-outbox-disabled]` | Queue metrics off (`_queue: false`) — asserts no `queue.*` datapoints are ever exported. | +| `[tracing-in-memory]` | Wires `MyInMemorySpanExporter` as the trace exporter. | +| `[sampler-ignore-authors]` | Adds `/odata/v4/admin/Authors` to the sampler's `ignoreIncomingPaths`. | +| `[native-fetch]` | `remote.native_fetch = true` — routes outbound remote calls through native fetch (undici instrumentation) instead of the Cloud SDK. | +| `[no-scheduling]` | `requires.scheduling: false` — disables cds 10's default periodic outbox reads (they cause spurious passport set/reset pairs). | +| `[persistent-outbox]` | file-based messaging with a persistent outbox. | +| `[inboxed]` | file-based messaging with `inboxed: true` (producer- **and** consumer-side queue workers). | +| `[without-outbox]` | file-based messaging with `outboxed: false` (writes to the file directly from the producer tx). | + +> The `[multitenancy]` profile lives in the app's own `test/bookshop/package.json` (auth users + `multitenancy: true`), composed with the above where needed. + +### Load-order gotcha (from #486) + +`package.json` cds config is loaded **after** `.cdsrc.json` (last-writer-wins). So any base default that a profile must be able to override has to live in the **`.cdsrc.json` base**, not in `package.json` — otherwise `package.json` would clobber the profile. #486 moved the base `cds.log` / `messaging` defaults into `.cdsrc.json` for exactly this reason. + +> **Do not** reintroduce `process.env.cds_*` string-JSON config. That fragile pattern (config via stringified JSON in env vars, order-sensitive against the `@sap/cds` require) was removed in #486. Use profiles. + +## In-memory test infrastructure + +Two exporter-shaped classes capture telemetry into module-level arrays that tests import directly — asserting on **structured spans/datapoints**, never scraping `console.dir` output: + +- **[`test/bookshop/lib/MyInMemorySpanExporter.js`](test/bookshop/lib/MyInMemorySpanExporter.js)** — spans accumulate in `captured`; helpers `groupedByTrace()` / `rootSpans()` / `reset()`. Wired via the `tracing-in-memory` profile. +- **[`test/bookshop/lib/MyInMemoryMetricReader.js`](test/bookshop/lib/MyInMemoryMetricReader.js)** — metrics captured via the metrics profiles. It honors **DELTA temporality**, matching production (`lib/metrics/index.js` configures the real exporter with `AggregationTemporality.DELTA`), so the tests validate the real export shape. Under DELTA, counter datapoints report only the increment since the last collection, so the reader folds SUM increments into running totals while GAUGE datapoints keep their latest absolute value. + +**KEY RULE:** neither module may `require('@sap/cds')` at module top. Doing so once broke span capture — the cds require has to happen inside the test file, *after* the profile is applied. Both modules stay dependency-light (only `@opentelemetry/*` primitives + node timers). + +Cross-file correctness of the metric reader's process-level singletons relies on Vitest isolating each file in its own worker (`pool: 'forks'`, `isolate: true`); two files sharing the module in one process would bleed counter totals together. + +## Shared test helpers — `test/utils.js` + +Centralized in [`test/utils.js`](test/utils.js) (added in #488) so the ~10 tracing/metrics suites stop copy-pasting them. See the doc comments at each definition for full detail: + +- **`flushSpans()`** — force-flush the tracer provider's span processor so buffered spans reach `captured`. +- **`eventually(fn, { flush, timeout, interval })`** — state-based wait: repeatedly flush + re-run the assertion until it holds or times out. Replaces fixed `wait(...)` sleeps that flake on HANA (background/spawned work flushes after any reasonable fixed window). `flush` defaults to `flushSpans`. +- **`makeExpectEventually(flush, { timeout, interval })`** — builds an `expectEventually(assertion)` bound to a specific flush target + poll defaults (metric suites pass the reader's `forceFlush`). +- **`clearOutbox(timeout)`** — best-effort, timeout-bounded outbox `DELETE` that can never hang the surrounding hook (a draining HANA pool could otherwise block indefinitely). +- **`asExternalClient(fn)`** — runs a client request under `suppressTracing`. The in-process test client would otherwise create an outgoing **CLIENT** span for every request (an artificial extra root that also overwrites any manually-set `traceparent`). Real callers are separate, un-instrumented processes; this models that so the incoming SERVER span is created normally and stays the trace root. +- **`isOutboxScanTrace(g)` / `meaningful(groups)`** — filter out the queue scheduler's pure outbox-scan bookkeeping traces (a `db - tx` root touching only `cds.outbox.Messages`) so exact root-count assertions stay stable on the shared HANA container. + +**The flush + poll pattern:** instead of `await wait(500)` then asserting, wrap assertions in `eventually`/`expectEventually` — it flushes, checks, and returns the instant the state holds (fast on sqlite, resilient to HANA's variable worker latency). + +## HTTP instrumentation (from #475) + +HTTP instrumentation is enabled in the test app. Consequences the tests rely on: + +- **Incoming** requests produce a **SERVER** span that becomes each request trace's **root**; existing ` - tx` spans reparent under it (reparenting). The SERVER span also adopts the W3C trace context from an incoming `traceparent` header. +- **Outgoing** requests produce **CLIENT** spans. + +Because the test HTTP client runs in-process, its outgoing requests would themselves create CLIENT-span roots and pollute the trace. Tests therefore wrap client requests in **`asExternalClient`** (see above) to model an external, un-instrumented caller. + +## Sanctioned skips + +Only **two** skips are allowed (per #477). Any *new* skip must be justified against this bar; everything else that is skipped is tracked debt. + +1. **SAP Passport** — [`test/passport.test.js`](test/passport.test.js) skips on **sqlite** (`db.kind === 'sqlite'`). SAP Passport is a HANA session-context feature with no sqlite equivalent; it runs on HANA. +2. **Multitenancy on HANA** — `tracing-mt.test.js` and `metrics-outbox-multitenant.test.js` are **excluded from the HANA job** (in `vitest.config.mjs`). MTX tenant subscription needs a bound BTP Service Manager to provision per-tenant HDI containers, which the single pre-provisioned HDI container in CI lacks. They run fully on sqlite (in-memory tenants). + +This is the inverse pairing: passport is sqlite-skip / HANA-run; multitenancy is HANA-skip / sqlite-run. + +Other skips are **debt tracked in #477**, not sanctioned exceptions: + +- **§1 — queue-worker tracing on sqlite:** `tracing-scheduled`, `tracing-outboxed-batch`, `tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox` skip their worker-span cases on sqlite. Published `@sap/cds` uses a raw `setTimeout` bypass (not `cds.spawn`) for the sqlite queue worker to avoid a single-writer deadlock, so the `cds.spawn - run task` root span never appears. Gated on a cds queue-spawn fix landing; remove with a follow-up. +- **§3 — unimplemented stubs:** placeholder `test.skip` cases in `tracing.test.js` and `tracing-mt.test.js` (individual handlers, remote, `$batch`, `srv.emit`, `cds.spawn` under multitenancy) — real coverage gaps to be written. + +## Known caveats & gotchas + +- **`startup > NO_TELEMETRY=true` local artifact.** [`test/startup.test.js`](test/startup.test.js) shells out to `cds serve` with env overrides. In some local shells the `NO_TELEMETRY=true` case can fail due to inherited environment; it passes in CI and in a clean environment. This is a pre-existing local-env artifact, not a product bug. +- **Internal-registry lockfile trap for `@sap/*` installs.** Installing `@sap/*` packages against SAP's internal registry can rewrite `package-lock.json` to internal URLs. Always install against the public npm registry and verify the lockfile is clean before committing: + + ```sh + grep -c int.repositories.cloud.sap package-lock.json # must be 0 + ``` diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 3f7956bf..77a4310b 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -25,9 +25,8 @@ function metricValue(tenant, metric) { // wired, so a misconfigured profile fails loudly instead of busy-spinning the full timeout. const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) -// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. -// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this -// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). +// Multitenancy runs on sqlite only; excluded from the HANA job (needs a bound Service Manager). +// See TESTING.md → Sanctioned skips. describe('queue metrics for multi tenant service', () => { const T1 = 'tenant_1' const T2 = 'tenant_2' diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index 399a335e..b565e21f 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -55,11 +55,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans. + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index bb00d588..2cdcb212 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -99,11 +99,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans. + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index ef12a8a0..91a9fecc 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -22,11 +22,8 @@ module.exports = (CASE, CHECK) => { }) afterAll(async () => { - // On the shared HANA HDI container, a still-draining background queue worker from THIS file - // would dispatch into the NEXT file's run and add foreign `cds.spawn - run task` roots that - // break its exact root-count CHECKs. Clear the shared outbox, let the last worker settle, then - // clear again. Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the - // hook. HANA-only: sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + // HANA-only outbox settle so a draining worker can't bleed into the next file; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). if (process.env.TELEMETRY_TEST_HANA) { await clearOutbox() await wait(5000) @@ -36,11 +33,8 @@ module.exports = (CASE, CHECK) => { }) beforeEach(async () => { - // Clear any outbox rows left behind by a prior test file BEFORE resetting the span buffer. - // The single HANA HDI container is shared across all files, so a leftover message would be - // dispatched by THIS file's queue worker — producing a foreign `cds.spawn - run task` root - // that breaks the exact root-count CHECKs. Reset AFTER so the DELETE's own spans aren't - // captured. (No-op on sqlite, where each file gets its own in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await clearOutbox() reset() }) diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index 8aa91e74..4eb412c1 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -4,9 +4,8 @@ const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') -// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. -// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this -// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). +// Multitenancy runs on sqlite only; excluded from the HANA job (needs a bound Service Manager). +// See TESTING.md → Sanctioned skips. describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' const TENANT2 = 'tenant_2' diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index a7a4d44c..5a8405f3 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -9,11 +9,8 @@ const { hrTimeToNanoseconds } = require('@opentelemetry/core') const { eventually } = require('./utils') describe('tracing for outboxed batch (chunk-size fan-out)', () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans (captured stays empty). + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return @@ -25,10 +22,8 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { }) beforeEach(async () => { - // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA - // HDI container is shared across all files, so a leftover message would be dispatched by this - // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own - // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await DELETE.from('cds.outbox.Messages') reset() }) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index 31c33182..df4c0c34 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -26,14 +26,8 @@ const otel = require('@opentelemetry/api') const { asExternalClient, eventually } = require('./utils') describe('tracing for scheduled tasks', () => { - // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite - // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite - // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds - // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans (captured stays empty). + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return @@ -45,10 +39,8 @@ describe('tracing for scheduled tasks', () => { }) beforeEach(async () => { - // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA - // HDI container is shared across all files, so a leftover message would be dispatched by this - // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own - // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await DELETE.from('cds.outbox.Messages') reset() })