mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-04 02:37:46 +02:00
ci: V8 E2E coverage + per-spec impact map (DEVP-205) (#31441)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc53dc101f
commit
d57545d4d0
1
.github/test-metrics/e2e-impact-map.json
vendored
Normal file
1
.github/test-metrics/e2e-impact-map.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,13 @@
|
|||
name: 'Test: E2E Coverage Weekly'
|
||||
name: 'Test: E2E Coverage Nightly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 1' # Every Monday at 2 AM
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
- cron: '0 2 * * *' # Nightly at 02:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare-docker:
|
||||
|
|
@ -14,17 +18,43 @@ jobs:
|
|||
runner: blacksmith-8vcpu-ubuntu-2204
|
||||
secrets: inherit
|
||||
|
||||
# Distribute the full e2e suite across shards by historical duration
|
||||
# (janitor orchestrator) instead of Playwright's count-based --shard=N/total.
|
||||
# Count-based sharding stacked the heavy + flaky specs onto one shard, which
|
||||
# then blew past the timeout; duration-weighting keeps every shard even.
|
||||
generate-matrix:
|
||||
name: Generate shard matrix
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
outputs:
|
||||
matrix: ${{ steps.gen.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup and Build janitor
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm turbo run build --filter=@n8n/playwright-janitor
|
||||
|
||||
- name: Generate matrix (8 shards, duration-weighted)
|
||||
id: gen
|
||||
run: |
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 8 --orchestrate)
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
name: E2E (coverage)
|
||||
needs: prepare-docker
|
||||
needs: [prepare-docker, generate-matrix]
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
test-mode: docker-artifact
|
||||
test-command: pnpm --filter=n8n-playwright test:container:coverage
|
||||
# Runs the coverage project + resolves this shard's V8 to lcov/per-spec
|
||||
# (keeps test-e2e-reusable coverage-agnostic). See run-coverage-shard.mjs.
|
||||
test-command: pnpm --filter=n8n-playwright coverage:shard
|
||||
workers: '1'
|
||||
runner: blacksmith-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 55
|
||||
pre-generated-matrix: '[{"shard":1,"images":""},{"shard":2,"images":""},{"shard":3,"images":""},{"shard":4,"images":""},{"shard":5,"images":""},{"shard":6,"images":""}]'
|
||||
pre-generated-matrix: ${{ needs.generate-matrix.outputs.matrix }}
|
||||
artifact-prefix: coverage
|
||||
build-variant: coverage
|
||||
secrets: inherit
|
||||
|
|
@ -47,18 +77,15 @@ jobs:
|
|||
pattern: coverage-shard-*
|
||||
path: /tmp/shards/
|
||||
|
||||
- name: Collect coverage JSON
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p packages/testing/playwright/.nyc_output/coverage
|
||||
found=$(find /tmp/shards -path '*/.nyc_output/coverage/*.json' 2>/dev/null | wc -l)
|
||||
echo "Found $found coverage JSON files across shards"
|
||||
find /tmp/shards -path '*/.nyc_output/coverage/*.json' \
|
||||
-exec cp {} packages/testing/playwright/.nyc_output/coverage/ \;
|
||||
ls -la packages/testing/playwright/.nyc_output/coverage/ || true
|
||||
- name: Build janitor (tested merge-coverage)
|
||||
run: pnpm turbo run build --filter=@n8n/playwright-janitor
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: pnpm --filter n8n-playwright coverage:report
|
||||
# Report (shard lcovs → unified lcov) + impact map (per-spec lcovs →
|
||||
# spec-keyed map), both via the property-tested janitor merge-coverage.
|
||||
# Logic lives in the tested aggregate-coverage.mjs, not inline bash.
|
||||
- name: Aggregate coverage + build impact map
|
||||
working-directory: packages/testing/playwright
|
||||
run: node scripts/aggregate-coverage.mjs --shards=/tmp/shards --out=coverage
|
||||
|
||||
- name: Upload Coverage Report Artifact
|
||||
if: always()
|
||||
6
.github/workflows/test-e2e-reusable.yml
vendored
6
.github/workflows/test-e2e-reusable.yml
vendored
|
|
@ -148,12 +148,10 @@ jobs:
|
|||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
packages/testing/playwright/.nyc_output/
|
||||
packages/testing/playwright/coverage/lcov.info
|
||||
packages/testing/playwright/coverage/by-spec/
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
# upload-artifact@v7 defaults this to false, which silently drops
|
||||
# `.nyc_output/` (dotfile) — needed for the weekly coverage workflow.
|
||||
include-hidden-files: true
|
||||
|
||||
- name: Cancel Currents run if workflow is cancelled
|
||||
if: ${{ cancelled() }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@ node_modules
|
|||
tmp
|
||||
dist
|
||||
coverage
|
||||
coverage-by-spec
|
||||
npm-debug.log*
|
||||
yarn.lock
|
||||
google-generated-credentials.json
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"scripts": {
|
||||
"clean": "rimraf dist .turbo .build",
|
||||
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"build:coverage": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=14000\" BUILD_WITH_COVERAGE=true vite build",
|
||||
"build:coverage": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=10240\" BUILD_WITH_COVERAGE=true vite build",
|
||||
"typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" vue-tsc --noEmit",
|
||||
"typecheck:watch": "vue-tsc --watch --noEmit",
|
||||
"dev": "pnpm serve",
|
||||
|
|
@ -146,7 +146,6 @@
|
|||
"sass-embedded": "catalog:",
|
||||
"unplugin-icons": "catalog:frontend",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-istanbul": "^8.0.0",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-static-copy": "4.0.0",
|
||||
"vite-svg-loader": "catalog:frontend",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { defineConfig, mergeConfig, type UserConfig } from 'vite';
|
|||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
import istanbul from 'vite-plugin-istanbul';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import { codecovVitePlugin } from '@codecov/vite-plugin';
|
||||
|
||||
|
|
@ -95,18 +94,6 @@ const plugins: UserConfig['plugins'] = [
|
|||
compiler: 'vue3',
|
||||
autoInstall: NODE_ENV === 'development',
|
||||
}),
|
||||
// Add istanbul coverage plugin for E2E tests
|
||||
...(process.env.BUILD_WITH_COVERAGE === 'true'
|
||||
? [
|
||||
istanbul({
|
||||
include: 'src/**/*',
|
||||
exclude: ['node_modules', 'tests/', 'dist/'],
|
||||
extension: ['.js', '.ts', '.vue'],
|
||||
forceBuildInstrument: true,
|
||||
requireEnv: false,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
|
|
@ -247,7 +234,9 @@ export default mergeConfig(
|
|||
},
|
||||
build: {
|
||||
minify: !!release,
|
||||
sourcemap: !!release,
|
||||
// Coverage builds emit INLINE maps so browser V8 coverage carries the
|
||||
// map in the script source and monocart resolves offsets back to src.
|
||||
sourcemap: process.env.BUILD_WITH_COVERAGE === 'true' ? 'inline' : !!release,
|
||||
target,
|
||||
},
|
||||
optimizeDeps: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { chmodSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { PortWithOptionalBinding, StartedNetwork, StartedTestContainer } from 'testcontainers';
|
||||
import { GenericContainer } from 'testcontainers';
|
||||
|
||||
|
|
@ -12,6 +14,10 @@ import { TEST_CONTAINER_IMAGES } from '../test-containers';
|
|||
import type { FileToMount } from './types';
|
||||
|
||||
const N8N_IMAGE = TEST_CONTAINER_IMAGES.n8n;
|
||||
|
||||
// In-container path that NODE_V8_COVERAGE writes to when coverage collection is
|
||||
// enabled (via StackConfig.coverageHostDir); bind-mounted to a host subdir.
|
||||
const CONTAINER_COVERAGE_DIR = '/cov';
|
||||
// Must match N8N_PORT / QUEUE_HEALTH_CHECK_PORT defaults.
|
||||
const N8N_READINESS_PORT = 5678;
|
||||
const N8N_STARTUP_TIMEOUT_MS = 60_000;
|
||||
|
|
@ -73,6 +79,7 @@ export interface N8NInstancesOptions {
|
|||
/** Resource quota for webhook procs. Falls back to `resourceQuota` if omitted. */
|
||||
webhookResourceQuota?: { memory?: number; cpu?: number };
|
||||
filesToMount?: FileToMount[];
|
||||
coverageHostDir?: string;
|
||||
}
|
||||
|
||||
export interface N8NInstancesResult {
|
||||
|
|
@ -142,6 +149,7 @@ interface SharedConfig {
|
|||
network: StartedNetwork;
|
||||
resourceQuota?: { memory?: number; cpu?: number };
|
||||
filesToMount?: FileToMount[];
|
||||
coverageHostDir?: string;
|
||||
}
|
||||
|
||||
interface ContainerStartResult {
|
||||
|
|
@ -162,7 +170,8 @@ async function createContainer(
|
|||
diagnostics: N8NStartupDiagnostics,
|
||||
): Promise<ContainerStartResult> {
|
||||
const { name, role, instanceNumber, networkAlias, hostPort } = instance;
|
||||
const { projectName, environment, network, resourceQuota, filesToMount } = shared;
|
||||
const { projectName, environment, network, resourceQuota, filesToMount, coverageHostDir } =
|
||||
shared;
|
||||
const { consumer, throwWithLogs, getLogs } = createSilentLogConsumer();
|
||||
const { strategy: waitStrategy, getLastBody: getLastReadinessBody } = createReadinessProbe(
|
||||
'/healthz/readiness',
|
||||
|
|
@ -170,8 +179,12 @@ async function createContainer(
|
|||
{ startupTimeoutMs: N8N_STARTUP_TIMEOUT_MS, readTimeoutMs: N8N_READ_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
const containerEnvironment = coverageHostDir
|
||||
? { ...environment, NODE_V8_COVERAGE: CONTAINER_COVERAGE_DIR }
|
||||
: environment;
|
||||
|
||||
let container = new GenericContainer(N8N_IMAGE)
|
||||
.withEnvironment(environment)
|
||||
.withEnvironment(containerEnvironment)
|
||||
.withLabels({
|
||||
'com.docker.compose.project': projectName,
|
||||
'com.docker.compose.service': SERVICE_LABEL[role],
|
||||
|
|
@ -180,9 +193,24 @@ async function createContainer(
|
|||
.withPullPolicy(new N8nImagePullPolicy(N8N_IMAGE))
|
||||
.withName(name)
|
||||
.withLogConsumer(consumer)
|
||||
.withReuse()
|
||||
.withNetwork(network);
|
||||
|
||||
if (coverageHostDir) {
|
||||
// Per-container host dir → /cov; n8n flushes V8 here on graceful stop.
|
||||
// Reuse must stay off so the process actually exits and flushes.
|
||||
const hostCoverageDir = join(coverageHostDir, name);
|
||||
mkdirSync(hostCoverageDir, { recursive: true });
|
||||
// The n8n container runs as `node` (uid 1000); on Linux CI the bind mount
|
||||
// is direct (no Docker Desktop uid mapping), so make the dir writable by
|
||||
// the container or NODE_V8_COVERAGE silently fails to flush.
|
||||
chmodSync(hostCoverageDir, 0o777);
|
||||
container = container.withBindMounts([
|
||||
{ source: hostCoverageDir, target: CONTAINER_COVERAGE_DIR, mode: 'rw' },
|
||||
]);
|
||||
} else {
|
||||
container = container.withReuse();
|
||||
}
|
||||
|
||||
if (filesToMount?.length) {
|
||||
container = container.withCopyContentToContainer(filesToMount);
|
||||
}
|
||||
|
|
@ -242,6 +270,7 @@ export async function createN8NInstances(
|
|||
workerResourceQuota,
|
||||
webhookResourceQuota,
|
||||
filesToMount,
|
||||
coverageHostDir,
|
||||
} = options;
|
||||
|
||||
const log = createElapsedLogger('n8n-instances');
|
||||
|
|
@ -255,6 +284,7 @@ export async function createN8NInstances(
|
|||
network,
|
||||
resourceQuota,
|
||||
filesToMount,
|
||||
coverageHostDir,
|
||||
};
|
||||
|
||||
const workerShared: SharedConfig = {
|
||||
|
|
@ -263,6 +293,7 @@ export async function createN8NInstances(
|
|||
network,
|
||||
resourceQuota: workerResourceQuota ?? resourceQuota,
|
||||
filesToMount,
|
||||
coverageHostDir,
|
||||
};
|
||||
|
||||
const webhookShared: SharedConfig = {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,13 @@ export interface StackConfig {
|
|||
* Benchmarks should set `'round_robin'` to actually distribute load.
|
||||
*/
|
||||
lbPolicy?: LoadBalancerPolicy;
|
||||
/**
|
||||
* When set, each n8n container collects Node V8 coverage: `NODE_V8_COVERAGE`
|
||||
* is written to a per-container subdir of this host path (bind-mounted), reuse
|
||||
* is disabled, and the stack stops gracefully so the process flushes on exit.
|
||||
* Opt-in capability for the coverage pipeline; off by default.
|
||||
*/
|
||||
coverageHostDir?: string;
|
||||
}
|
||||
|
||||
export interface Service<TResult extends ServiceResult = ServiceResult> {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
|||
services: enabledServices = [],
|
||||
external = false,
|
||||
networkName,
|
||||
coverageHostDir,
|
||||
} = config;
|
||||
|
||||
const log = createElapsedLogger('stack');
|
||||
|
|
@ -228,6 +229,7 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
|||
workerResourceQuota,
|
||||
webhookResourceQuota,
|
||||
filesToMount,
|
||||
coverageHostDir,
|
||||
});
|
||||
containers.push(...n8nResult.containers);
|
||||
telemetry.recordN8nStartup(
|
||||
|
|
@ -317,7 +319,7 @@ export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack>
|
|||
return {
|
||||
baseUrl,
|
||||
projectName: uniqueProjectName,
|
||||
stop: async () => await stopN8NStack(containers, network, uniqueProjectName),
|
||||
stop: async () => await stopN8NStack(containers, network, uniqueProjectName, coverageHostDir),
|
||||
containers,
|
||||
serviceResults,
|
||||
services: servicesProxy,
|
||||
|
|
@ -360,12 +362,18 @@ async function stopN8NStack(
|
|||
containers: StartedTestContainer[],
|
||||
network: StartedNetwork,
|
||||
uniqueProjectName: string,
|
||||
coverageHostDir?: string,
|
||||
): Promise<void> {
|
||||
const errors: Error[] = [];
|
||||
// testcontainers stops with timeout:0 (immediate SIGKILL). When collecting
|
||||
// Node V8 coverage we need a graceful SIGTERM + grace so n8n flushes
|
||||
// NODE_V8_COVERAGE to the bind-mounted dir before exit (~1s in practice).
|
||||
// testcontainers `timeout` is in milliseconds (→ docker stop -t seconds).
|
||||
const stopOptions = coverageHostDir ? { timeout: 30_000 } : undefined;
|
||||
try {
|
||||
const stopPromises = containers.reverse().map(async (container) => {
|
||||
try {
|
||||
await container.stop();
|
||||
await container.stop(stopOptions);
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
new Error(`Failed to stop container ${container.getId()}: ${getErrorMessage(error)}`),
|
||||
|
|
|
|||
|
|
@ -1,51 +1,53 @@
|
|||
{
|
||||
"name": "@n8n/playwright-janitor",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Static analysis and architecture enforcement for Playwright test suites",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"janitor": "bin/janitor.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"playwright",
|
||||
"testing",
|
||||
"static-analysis",
|
||||
"architecture",
|
||||
"linting"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@n8n/rules-engine": "workspace:*",
|
||||
"glob": "^10.0.0",
|
||||
"yaml": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ts-morph": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"ts-morph": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
"version": "0.1.0",
|
||||
"description": "Static analysis and architecture enforcement for Playwright test suites",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"janitor": "bin/janitor.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"playwright",
|
||||
"testing",
|
||||
"static-analysis",
|
||||
"architecture",
|
||||
"linting"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@n8n/rules-engine": "workspace:*",
|
||||
"glob": "^10.0.0",
|
||||
"yaml": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ts-morph": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"fast-check": "catalog:",
|
||||
"ts-morph": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ import {
|
|||
formatBaselineInfo,
|
||||
getBaselinePath,
|
||||
} from './core/baseline.js';
|
||||
import {
|
||||
type ImpactMap,
|
||||
type InternedImpactMap,
|
||||
decodeImpactMap,
|
||||
encodeImpactMap,
|
||||
mergeCoverage,
|
||||
resolveImpact,
|
||||
} from './core/coverage-map.js';
|
||||
import { extractDiffs } from './core/extract-diffs.js';
|
||||
import {
|
||||
ImpactAnalyzer,
|
||||
|
|
@ -646,6 +654,76 @@ function runScope(options: CliOptions): void {
|
|||
console.log(formatScope(result));
|
||||
}
|
||||
|
||||
function findLcovFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const p = path.join(dir, entry);
|
||||
if (fs.statSync(p).isDirectory()) out.push(...findLcovFiles(p));
|
||||
else if (entry.endsWith('.lcov') || entry === 'lcov.info') out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** merge-coverage: per-spec lcovs (under --inputs-dir) → unified lcov + impact map. */
|
||||
function runMergeCoverage(options: CliOptions): void {
|
||||
if (!options.inputsDir || !options.outLcov || !options.outMap) {
|
||||
console.error('Error: --inputs-dir, --out-lcov, and --out-map are required');
|
||||
process.exit(1);
|
||||
}
|
||||
const files = fs.existsSync(options.inputsDir) ? findLcovFiles(options.inputsDir) : [];
|
||||
// spec attribution comes from each lcov's TN:; the path is only a fallback.
|
||||
const inputs = files.map((f) => ({ text: fs.readFileSync(f, 'utf8'), spec: f }));
|
||||
const result = mergeCoverage(inputs);
|
||||
fs.writeFileSync(options.outLcov, result.lcov);
|
||||
// Interned on-disk form — spec paths once, referenced by index (~10x smaller).
|
||||
fs.writeFileSync(options.outMap, JSON.stringify(encodeImpactMap(result.impactMap)));
|
||||
console.error(
|
||||
`merge-coverage: ${files.length} lcov(s) → ${result.stats.files} files, ` +
|
||||
`${result.stats.mapEntries} map entries, ${result.stats.specs} specs`,
|
||||
);
|
||||
}
|
||||
|
||||
/** select-e2e: changed files + impact map → spec list (JSON).
|
||||
*
|
||||
* Two layers of safety, both biased to OVER-select (never miss a regression):
|
||||
* - FAIL-OPEN on the map source: a missing/unreadable/corrupt map → broad
|
||||
* (run everything). This is what makes swapping the committed file for a
|
||||
* remote webhook safe — a fetch failure degrades to running the full suite,
|
||||
* never to skipping tests.
|
||||
* - DEFAULT-BROAD on content: any changed file absent from a loaded map → broad.
|
||||
*/
|
||||
function runSelectE2e(options: CliOptions): void {
|
||||
const changed = (readChangedFiles(options) ?? []).map((file) => ({ file }));
|
||||
const allSpecs = options.allSpecsFile
|
||||
? fs
|
||||
.readFileSync(options.allSpecsFile, 'utf8')
|
||||
.split(/[\n,]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
let map: ImpactMap = {};
|
||||
let failOpen: string | undefined;
|
||||
if (options.mapFile && fs.existsSync(options.mapFile)) {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(fs.readFileSync(options.mapFile, 'utf8'));
|
||||
// Interned form ({specs, files}) is decoded; a plain ImpactMap is used as-is.
|
||||
const isInterned =
|
||||
typeof parsed === 'object' && parsed !== null && 'specs' in parsed && 'files' in parsed;
|
||||
map = isInterned ? decodeImpactMap(parsed as InternedImpactMap) : (parsed as ImpactMap);
|
||||
} catch (error) {
|
||||
failOpen = `unreadable map: ${String(error)}`;
|
||||
}
|
||||
} else {
|
||||
failOpen = options.mapFile ? `map not found: ${options.mapFile}` : 'no --map provided';
|
||||
}
|
||||
|
||||
// With an empty map every changed file is "unmapped" → resolveImpact returns
|
||||
// broad, so fail-open falls out of the same code path — no special-casing.
|
||||
const result = resolveImpact(changed, map, { allSpecs });
|
||||
console.log(JSON.stringify({ ...result, failOpen }));
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const options = parseArgs();
|
||||
|
||||
|
|
@ -704,6 +782,14 @@ async function main(): Promise<void> {
|
|||
runTestScopedCmd(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === 'merge-coverage') {
|
||||
runMergeCoverage(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === 'select-e2e') {
|
||||
runSelectE2e(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.list) {
|
||||
const runner = createDefaultRunner();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export type Command =
|
|||
| 'affected-packages'
|
||||
| 'scope'
|
||||
| 'test-scoped'
|
||||
| 'filter-shard';
|
||||
| 'filter-shard'
|
||||
| 'merge-coverage'
|
||||
| 'select-e2e';
|
||||
|
||||
export interface CliOptions {
|
||||
command: Command;
|
||||
|
|
@ -63,6 +65,12 @@ export interface CliOptions {
|
|||
passthroughArgs: string[];
|
||||
// filter-shard-specific options
|
||||
url?: string;
|
||||
// coverage map options (merge-coverage / select-e2e)
|
||||
inputsDir?: string;
|
||||
outLcov?: string;
|
||||
outMap?: string;
|
||||
mapFile?: string;
|
||||
allSpecsFile?: string;
|
||||
}
|
||||
|
||||
const SUBCOMMANDS: Record<string, Command> = {
|
||||
|
|
@ -78,6 +86,8 @@ const SUBCOMMANDS: Record<string, Command> = {
|
|||
scope: 'scope',
|
||||
'test-scoped': 'test-scoped',
|
||||
'filter-shard': 'filter-shard',
|
||||
'merge-coverage': 'merge-coverage',
|
||||
'select-e2e': 'select-e2e',
|
||||
};
|
||||
|
||||
interface FlagHandler {
|
||||
|
|
@ -207,6 +217,21 @@ const VALUE_FLAG_HANDLERS: Record<string, (options: CliOptions, value: string) =
|
|||
'--url=': (opts, value) => {
|
||||
opts.url = value;
|
||||
},
|
||||
'--inputs-dir=': (opts, value) => {
|
||||
opts.inputsDir = value;
|
||||
},
|
||||
'--out-lcov=': (opts, value) => {
|
||||
opts.outLcov = value;
|
||||
},
|
||||
'--out-map=': (opts, value) => {
|
||||
opts.outMap = value;
|
||||
},
|
||||
'--map=': (opts, value) => {
|
||||
opts.mapFile = value;
|
||||
},
|
||||
'--all-specs=': (opts, value) => {
|
||||
opts.allSpecsFile = value;
|
||||
},
|
||||
};
|
||||
|
||||
function createDefaultOptions(): CliOptions {
|
||||
|
|
|
|||
409
packages/testing/janitor/src/core/coverage-map.test.ts
Normal file
409
packages/testing/janitor/src/core/coverage-map.test.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
import fc from 'fast-check';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
type ChangedFile,
|
||||
type ImpactMap,
|
||||
type LcovInput,
|
||||
decodeImpactMap,
|
||||
encodeImpactMap,
|
||||
mergeCoverage,
|
||||
parseLcov,
|
||||
resolveImpact,
|
||||
} from './coverage-map.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model + generators
|
||||
//
|
||||
// A test "execution" = one spec running one function (file, fnLine) some number
|
||||
// of times. hits === 0 models a function that was LOADED but not executed (must
|
||||
// NOT be attributed). We build per-spec lcovs from a list of executions and
|
||||
// assert the map/selector against that ground truth.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Exec {
|
||||
spec: string;
|
||||
file: string;
|
||||
fnLine: number;
|
||||
hits: number;
|
||||
}
|
||||
|
||||
const fnName = (line: number) => `fn${line}`;
|
||||
|
||||
/** Bounded domains so specs/files/functions overlap — exercises merge + dedup. */
|
||||
const arbExec: fc.Arbitrary<Exec> = fc.record({
|
||||
spec: fc.integer({ min: 0, max: 4 }).map((n) => `tests/e2e/s${n}.spec.ts`),
|
||||
file: fc.integer({ min: 0, max: 3 }).map((n) => `packages/p${n}/src/f.ts`),
|
||||
fnLine: fc.integer({ min: 1, max: 6 }).map((n) => n * 10), // 10,20,...,60
|
||||
hits: fc.nat({ max: 5 }),
|
||||
});
|
||||
|
||||
// At most one exec per (spec, file, fnLine) so each per-spec lcov is well-formed.
|
||||
const arbExecs: fc.Arbitrary<Exec[]> = fc.uniqueArray(arbExec, {
|
||||
maxLength: 30,
|
||||
selector: (e) => `${e.spec}|${e.file}|${e.fnLine}`,
|
||||
});
|
||||
|
||||
/** Build per-spec lcov inputs from executions (one TN per spec, one SF per file). */
|
||||
function buildInputs(execs: Exec[]): LcovInput[] {
|
||||
const bySpec = new Map<string, Map<string, Exec[]>>();
|
||||
for (const e of execs) {
|
||||
const byFile = bySpec.get(e.spec) ?? new Map<string, Exec[]>();
|
||||
bySpec.set(e.spec, byFile);
|
||||
byFile.set(e.file, [...(byFile.get(e.file) ?? []), e]);
|
||||
}
|
||||
const inputs: LcovInput[] = [];
|
||||
for (const [spec, byFile] of bySpec) {
|
||||
const lines: string[] = [`TN:${spec}`];
|
||||
for (const [file, fns] of byFile) {
|
||||
lines.push(`SF:${file}`);
|
||||
for (const f of fns) lines.push(`FN:${f.fnLine},${fnName(f.fnLine)}`);
|
||||
for (const f of fns) lines.push(`FNDA:${f.hits},${fnName(f.fnLine)}`);
|
||||
for (const f of fns) lines.push(`DA:${f.fnLine},${f.hits}`);
|
||||
lines.push('end_of_record');
|
||||
}
|
||||
inputs.push({ text: lines.join('\n') + '\n', spec });
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
const shuffle = <T>(xs: T[], keys: number[]): T[] =>
|
||||
xs
|
||||
.map((x, i) => [x, keys[i] ?? 0] as const)
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.map(([x]) => x);
|
||||
|
||||
// ===========================================================================
|
||||
// Unit tests — concrete, readable contracts
|
||||
// ===========================================================================
|
||||
|
||||
describe('parseLcov', () => {
|
||||
it('parses TN/SF/FN/FNDA/DA into per-spec records', () => {
|
||||
const recs = parseLcov(
|
||||
'TN:spec-a\nSF:packages/cli/src/x.ts\nFN:5,foo\nFNDA:3,foo\nDA:5,3\nend_of_record\n',
|
||||
);
|
||||
expect(recs).toHaveLength(1);
|
||||
expect(recs[0]).toMatchObject({ spec: 'spec-a', file: 'packages/cli/src/x.ts' });
|
||||
expect(recs[0].fns).toEqual([{ name: 'foo', line: 5, hits: 3 }]);
|
||||
});
|
||||
|
||||
it('falls back to fallbackSpec when TN is absent or empty', () => {
|
||||
const recs = parseLcov('SF:a.ts\nFN:1,f\nFNDA:1,f\nend_of_record\n', 'fallback');
|
||||
expect(recs[0].spec).toBe('fallback');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeCoverage', () => {
|
||||
it('attributes a function to every spec that executed it (hits > 0)', () => {
|
||||
const { impactMap } = mergeCoverage([
|
||||
{ spec: 'A', text: 'TN:A\nSF:f.ts\nFN:10,fn\nFNDA:2,fn\nend_of_record\n' },
|
||||
{ spec: 'B', text: 'TN:B\nSF:f.ts\nFN:10,fn\nFNDA:1,fn\nend_of_record\n' },
|
||||
]);
|
||||
expect(impactMap['f.ts']['10']).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('excludes load-only functions (hits === 0) from the map', () => {
|
||||
const { impactMap } = mergeCoverage([
|
||||
{
|
||||
spec: 'A',
|
||||
text: 'TN:A\nSF:f.ts\nFN:10,hit\nFN:20,loaded\nFNDA:4,hit\nFNDA:0,loaded\nend_of_record\n',
|
||||
},
|
||||
]);
|
||||
expect(impactMap['f.ts']['10']).toEqual(['A']);
|
||||
expect(impactMap['f.ts']['20']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sums hit counts across specs in the unified lcov', () => {
|
||||
const { lcov } = mergeCoverage([
|
||||
{ spec: 'A', text: 'TN:A\nSF:f.ts\nFN:10,fn\nFNDA:2,fn\nDA:10,2\nend_of_record\n' },
|
||||
{ spec: 'B', text: 'TN:B\nSF:f.ts\nFN:10,fn\nFNDA:3,fn\nDA:10,3\nend_of_record\n' },
|
||||
]);
|
||||
expect(lcov).toContain('FNDA:5,fn');
|
||||
expect(lcov).toContain('DA:10,5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveImpact', () => {
|
||||
const map: ImpactMap = {
|
||||
'packages/cli/src/x.ts': { '10': ['spec-a'], '30': ['spec-b'] },
|
||||
};
|
||||
|
||||
it('selects only the specs for the function a changed line falls into', () => {
|
||||
expect(resolveImpact([{ file: 'packages/cli/src/x.ts', lines: [12] }], map).specs).toEqual([
|
||||
'spec-a',
|
||||
]);
|
||||
expect(resolveImpact([{ file: 'packages/cli/src/x.ts', lines: [35] }], map).specs).toEqual([
|
||||
'spec-b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('selects all of a file’s specs when no lines are given', () => {
|
||||
expect(resolveImpact([{ file: 'packages/cli/src/x.ts' }], map).specs).toEqual([
|
||||
'spec-a',
|
||||
'spec-b',
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats a change above the first function (top-level) as whole-file', () => {
|
||||
const r = resolveImpact([{ file: 'packages/cli/src/x.ts', lines: [1] }], map);
|
||||
expect(r.specs).toEqual(['spec-a', 'spec-b']);
|
||||
});
|
||||
|
||||
it('forces broad mode for an unmapped file (the safety valve)', () => {
|
||||
const r = resolveImpact([{ file: 'packages/new/src/new.ts', lines: [3] }], map, {
|
||||
allSpecs: ['spec-a', 'spec-b', 'spec-c'],
|
||||
});
|
||||
expect(r.mode).toBe('broad');
|
||||
expect(r.unmapped).toEqual(['packages/new/src/new.ts']);
|
||||
expect(r.specs).toEqual(['spec-a', 'spec-b', 'spec-c']);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Property + metamorphic tests — the soundness guarantee
|
||||
// ===========================================================================
|
||||
|
||||
describe('mergeCoverage — properties', () => {
|
||||
it('SOUNDNESS: every executed (spec, function) appears in the map', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
for (const e of execs) {
|
||||
if (e.hits > 0) {
|
||||
expect(impactMap[e.file]?.[String(e.fnLine)] ?? []).toContain(e.spec);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('NO PHANTOMS: a spec is not attributed to a function it only loaded (hits 0)', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
for (const e of execs) {
|
||||
if (e.hits === 0) {
|
||||
// (spec,file,fnLine) is unique per execs, so no hits>0 record revives it.
|
||||
expect(impactMap[e.file]?.[String(e.fnLine)] ?? []).not.toContain(e.spec);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('ORDER-INDEPENDENT: merge result does not depend on input order', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, fc.array(fc.nat(), { maxLength: 40 }), (execs, keys) => {
|
||||
const inputs = buildInputs(execs);
|
||||
const a = mergeCoverage(inputs);
|
||||
const b = mergeCoverage(shuffle(inputs, keys));
|
||||
expect(b.impactMap).toEqual(a.impactMap);
|
||||
expect(b.lcov).toEqual(a.lcov);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('MONOTONIC: adding coverage never removes a map entry', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, arbExecs, (a, b) => {
|
||||
const mapA = mergeCoverage(buildInputs(a)).impactMap;
|
||||
const mapAB = mergeCoverage([...buildInputs(a), ...buildInputs(b)]).impactMap;
|
||||
for (const file of Object.keys(mapA)) {
|
||||
for (const line of Object.keys(mapA[file])) {
|
||||
for (const spec of mapA[file][line]) {
|
||||
expect(mapAB[file]?.[line] ?? []).toContain(spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('IDEMPOTENT: merging duplicated inputs yields the same map', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const inputs = buildInputs(execs);
|
||||
expect(mergeCoverage([...inputs, ...inputs]).impactMap).toEqual(
|
||||
mergeCoverage(inputs).impactMap,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveImpact — properties', () => {
|
||||
it('SOUNDNESS: a change to an executed function selects every spec that ran it', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
for (const e of execs) {
|
||||
if (e.hits > 0) {
|
||||
const r = resolveImpact([{ file: e.file, lines: [e.fnLine] }], impactMap);
|
||||
expect(r.specs).toContain(e.spec);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('LINE-PRECISE ⊆ FILE-LEVEL: pinning a line never selects more than the whole file', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, fc.integer({ min: 1, max: 60 }), (execs, line) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
const files = Object.keys(impactMap);
|
||||
if (files.length === 0) return;
|
||||
const file = files[0];
|
||||
const precise = new Set(resolveImpact([{ file, lines: [line] }], impactMap).specs);
|
||||
const whole = new Set(resolveImpact([{ file }], impactMap).specs);
|
||||
for (const s of precise) expect(whole).toContain(s);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('MONOTONIC: resolving more changed files never selects fewer specs', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
const files = Object.keys(impactMap);
|
||||
if (files.length < 2) return;
|
||||
const a: ChangedFile[] = [{ file: files[0] }];
|
||||
const ab: ChangedFile[] = [{ file: files[0] }, { file: files[1] }];
|
||||
const specsA = new Set(resolveImpact(a, impactMap).specs);
|
||||
const specsAB = new Set(resolveImpact(ab, impactMap).specs);
|
||||
for (const s of specsA) expect(specsAB).toContain(s);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('DEFAULT-BROAD: any unmapped changed file forces the full spec set', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
arbExecs,
|
||||
fc.array(fc.string(), { minLength: 1, maxLength: 5 }),
|
||||
(execs, allSpecs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
const r = resolveImpact([{ file: 'packages/never/covered.ts' }], impactMap, { allSpecs });
|
||||
expect(r.mode).toBe('broad');
|
||||
for (const s of allSpecs) expect(r.specs).toContain(s);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('R9: lines:[] (empty array) resolves to whole-file, not empty', () => {
|
||||
const map: ImpactMap = { 'f.ts': { '10': ['A'], '20': ['B'] } };
|
||||
// Distinct from undefined; must not silently select nothing (under-selection).
|
||||
expect(resolveImpact([{ file: 'f.ts', lines: [] }], map).specs).toEqual(['A', 'B']);
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Serializer + parser hardening — the serializer is exercised by every merge
|
||||
// but the algebra properties use mergeCoverage as their own oracle, so a
|
||||
// wrong-but-deterministic serialization slips through. These pin it externally.
|
||||
// ===========================================================================
|
||||
|
||||
describe('serializeLcov (via mergeCoverage.lcov)', () => {
|
||||
it('SERIALIZE-CORRECT: parse∘serialize round-trips FN/FNDA/DA hits', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
// Expected summed hits per (file, line) from the ground truth.
|
||||
const expected = new Map<string, Map<number, number>>();
|
||||
for (const e of execs) {
|
||||
const f = expected.get(e.file) ?? new Map<number, number>();
|
||||
expected.set(e.file, f);
|
||||
f.set(e.fnLine, (f.get(e.fnLine) ?? 0) + e.hits);
|
||||
}
|
||||
const { lcov } = mergeCoverage(buildInputs(execs));
|
||||
for (const rec of parseLcov(lcov)) {
|
||||
const exp = expected.get(rec.file);
|
||||
if (!exp) continue;
|
||||
for (const fn of rec.fns) expect(fn.hits).toBe(exp.get(fn.line) ?? 0);
|
||||
for (const ln of rec.lines) expect(ln.hits).toBe(exp.get(ln.line) ?? 0);
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits FN/DA sorted by line and correct FNF/FNH/LF/LH', () => {
|
||||
const { lcov } = mergeCoverage([
|
||||
{
|
||||
spec: 'A',
|
||||
text: 'TN:A\nSF:f.ts\nFN:30,c\nFN:10,a\nFN:20,b\nFNDA:0,c\nFNDA:5,a\nFNDA:2,b\nDA:30,0\nDA:10,5\nDA:20,2\nend_of_record\n',
|
||||
},
|
||||
]);
|
||||
const lines = lcov.split('\n');
|
||||
// Sort order is asserted externally — the order-independence property can't
|
||||
// catch a wrong comparator (it compares the serializer against itself).
|
||||
expect(lines.filter((l) => l.startsWith('FN:'))).toEqual(['FN:10,a', 'FN:20,b', 'FN:30,c']);
|
||||
expect(lines.filter((l) => l.startsWith('DA:'))).toEqual(['DA:10,5', 'DA:20,2', 'DA:30,0']);
|
||||
expect(lcov).toContain('FNF:3'); // 3 functions found
|
||||
expect(lcov).toContain('FNH:2'); // 2 hit (a,b; c has 0)
|
||||
expect(lcov).toContain('LF:3');
|
||||
expect(lcov).toContain('LH:2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('encode/decode impact map (interned on-disk form)', () => {
|
||||
it('LOSSLESS: decode∘encode round-trips any impact map', () => {
|
||||
fc.assert(
|
||||
fc.property(arbExecs, (execs) => {
|
||||
const { impactMap } = mergeCoverage(buildInputs(execs));
|
||||
expect(decodeImpactMap(encodeImpactMap(impactMap))).toEqual(impactMap);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('interns each spec path exactly once', () => {
|
||||
const { impactMap } = mergeCoverage([
|
||||
{ spec: 'A', text: 'TN:A\nSF:f.ts\nFN:10,a\nFN:20,b\nFNDA:1,a\nFNDA:1,b\nend_of_record\n' },
|
||||
]);
|
||||
const enc = encodeImpactMap(impactMap);
|
||||
expect(enc.specs).toEqual(['A']); // one entry despite two functions
|
||||
expect(enc.files['f.ts']['10']).toEqual([0]);
|
||||
expect(enc.files['f.ts']['20']).toEqual([0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseLcov hardening', () => {
|
||||
it('P5: never throws on arbitrary or malformed text', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.string(), (text) => {
|
||||
parseLcov(text); // must not throw
|
||||
}),
|
||||
);
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.constantFrom(
|
||||
'TN:',
|
||||
'TN:x',
|
||||
'SF:f.ts',
|
||||
'FN:bad',
|
||||
'FN:1,n',
|
||||
'FNDA:',
|
||||
'FNDA:x,n',
|
||||
'DA:',
|
||||
'DA:,',
|
||||
'end_of_record',
|
||||
'',
|
||||
'noise',
|
||||
),
|
||||
{ maxLength: 40 },
|
||||
),
|
||||
(lines) => {
|
||||
parseLcov(lines.join('\n'));
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('P4: FNDA without a matching FN → line 0, and fnLine resets per SF (no bleed)', () => {
|
||||
const recs = parseLcov(
|
||||
'SF:a.ts\nFN:5,shared\nFNDA:1,shared\nend_of_record\nSF:b.ts\nFNDA:2,shared\nend_of_record\n',
|
||||
);
|
||||
const b = recs.find((r) => r.file === 'b.ts')!;
|
||||
// `shared` is at line 5 in a.ts; b.ts has no FN for it → line 0, not 5.
|
||||
expect(b.fns).toEqual([{ name: 'shared', line: 0, hits: 2 }]);
|
||||
});
|
||||
});
|
||||
293
packages/testing/janitor/src/core/coverage-map.ts
Normal file
293
packages/testing/janitor/src/core/coverage-map.ts
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
/**
|
||||
* Coverage impact map — the substrate for E2E test selection.
|
||||
*
|
||||
* Pure functions (no I/O) so they can be exhaustively unit/property tested:
|
||||
* parseLcov — lcov text → per-spec coverage records
|
||||
* mergeCoverage — per-spec lcovs → unified lcov + bidirectional impact map
|
||||
* resolveImpact — changed files (± lines) → the E2E specs that must run
|
||||
*
|
||||
* SOUNDNESS is the contract everything is tested against: the selected set must
|
||||
* be a SUPERSET of the truly-affected set. Under-selection silently drops a real
|
||||
* regression; over-selection only wastes CI. So resolveImpact errs broad — an
|
||||
* unmapped (new/never-covered) file forces the full set, never an empty one.
|
||||
*
|
||||
* Attribution is keyed on the SPEC that exercised the code (lcov `TN:`), so a
|
||||
* git diff resolves straight to the spec files that cover the touched functions.
|
||||
*/
|
||||
|
||||
export interface FileCoverage {
|
||||
/** function name → { definition line, summed hit count } */
|
||||
fns: Map<string, { line: number; hits: number }>;
|
||||
/** source line → summed hit count */
|
||||
lines: Map<number, number>;
|
||||
}
|
||||
|
||||
/** file → function-start-line (as string) → specs that executed that function */
|
||||
export type ImpactMap = Record<string, Record<string, string[]>>;
|
||||
|
||||
/**
|
||||
* On-disk form of {@link ImpactMap}: spec paths are interned into `specs` and
|
||||
* referenced by index. Lossless — {@link decodeImpactMap} reconstructs the
|
||||
* ImpactMap exactly. Spec paths repeat across thousands of (file, line) entries,
|
||||
* so interning shrinks the artifact ~10x with no loss of the function-level
|
||||
* detail that line-precise selection and cross-layer overlap analysis need.
|
||||
*/
|
||||
export interface InternedImpactMap {
|
||||
specs: string[];
|
||||
files: Record<string, Record<string, number[]>>;
|
||||
}
|
||||
|
||||
/** One per-spec lcov and the spec it belongs to (used when records carry no `TN:`). */
|
||||
export interface LcovInput {
|
||||
text: string;
|
||||
spec: string;
|
||||
}
|
||||
|
||||
export interface MergeResult {
|
||||
lcov: string;
|
||||
impactMap: ImpactMap;
|
||||
stats: { files: number; functions: number; lines: number; specs: number; mapEntries: number };
|
||||
}
|
||||
|
||||
interface LcovRecord {
|
||||
spec: string;
|
||||
file: string;
|
||||
fns: Array<{ name: string; line: number; hits: number }>;
|
||||
lines: Array<{ line: number; hits: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse lcov text into per-spec records. A record's spec is its `TN:` if present
|
||||
* and non-empty, else `fallbackSpec`. Supports many `SF:` records per text.
|
||||
*/
|
||||
export function parseLcov(text: string, fallbackSpec = ''): LcovRecord[] {
|
||||
const records: LcovRecord[] = [];
|
||||
let spec = fallbackSpec;
|
||||
let current: LcovRecord | null = null;
|
||||
const fnLine = new Map<string, number>();
|
||||
for (const raw of text.split('\n')) {
|
||||
if (raw.startsWith('TN:')) {
|
||||
const tn = raw.slice(3).trim();
|
||||
spec = tn || fallbackSpec;
|
||||
} else if (raw.startsWith('SF:')) {
|
||||
fnLine.clear();
|
||||
current = { spec, file: raw.slice(3).trim(), fns: [], lines: [] };
|
||||
} else if (!current) {
|
||||
continue;
|
||||
} else if (raw.startsWith('FN:')) {
|
||||
const idx = raw.indexOf(',');
|
||||
const line = Number(raw.slice(3, idx));
|
||||
const name = raw.slice(idx + 1);
|
||||
fnLine.set(name, line);
|
||||
} else if (raw.startsWith('FNDA:')) {
|
||||
const idx = raw.indexOf(',');
|
||||
const hits = Number(raw.slice(5, idx));
|
||||
const name = raw.slice(idx + 1);
|
||||
current.fns.push({ name, line: fnLine.get(name) ?? 0, hits });
|
||||
} else if (raw.startsWith('DA:')) {
|
||||
const [line, hits] = raw.slice(3).split(',');
|
||||
current.lines.push({ line: Number(line), hits: Number(hits) });
|
||||
} else if (raw.startsWith('end_of_record')) {
|
||||
records.push(current);
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
if (current) records.push(current);
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge per-spec lcovs into one unified lcov + the impact map. Hit counts sum;
|
||||
* the map records, per function, the SET of specs that executed it (hits > 0).
|
||||
* Output is deterministic (sorted) so the merge is order-independent.
|
||||
*/
|
||||
export function mergeCoverage(inputs: LcovInput[]): MergeResult {
|
||||
const files = new Map<string, FileCoverage>();
|
||||
const funcToSpecs = new Map<string, Set<string>>();
|
||||
const allSpecs = new Set<string>();
|
||||
|
||||
const getFile = (sf: string): FileCoverage => {
|
||||
let f = files.get(sf);
|
||||
if (!f) files.set(sf, (f = { fns: new Map(), lines: new Map() }));
|
||||
return f;
|
||||
};
|
||||
|
||||
for (const input of inputs) {
|
||||
for (const rec of parseLcov(input.text, input.spec)) {
|
||||
allSpecs.add(rec.spec);
|
||||
const f = getFile(rec.file);
|
||||
for (const fn of rec.fns) {
|
||||
const cur = f.fns.get(fn.name) ?? { line: fn.line, hits: 0 };
|
||||
cur.hits += fn.hits;
|
||||
f.fns.set(fn.name, cur);
|
||||
if (fn.hits > 0) {
|
||||
const key = `${rec.file}#${fn.line}`;
|
||||
let specs = funcToSpecs.get(key);
|
||||
if (!specs) funcToSpecs.set(key, (specs = new Set()));
|
||||
specs.add(rec.spec);
|
||||
}
|
||||
}
|
||||
for (const ln of rec.lines) {
|
||||
f.lines.set(ln.line, (f.lines.get(ln.line) ?? 0) + ln.hits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lcov: serializeLcov(files),
|
||||
impactMap: buildImpactMap(funcToSpecs),
|
||||
stats: {
|
||||
files: files.size,
|
||||
functions: [...files.values()].reduce((n, f) => n + f.fns.size, 0),
|
||||
lines: [...files.values()].reduce((n, f) => n + f.lines.size, 0),
|
||||
specs: allSpecs.size,
|
||||
mapEntries: funcToSpecs.size,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function serializeLcov(files: Map<string, FileCoverage>): string {
|
||||
const out: string[] = [];
|
||||
for (const sf of [...files.keys()].sort()) {
|
||||
const { fns, lines } = files.get(sf)!;
|
||||
out.push('TN:', `SF:${sf}`);
|
||||
const fnEntries = [...fns.entries()].sort(
|
||||
(a, b) => a[1].line - b[1].line || a[0].localeCompare(b[0]),
|
||||
);
|
||||
for (const [name, { line }] of fnEntries) out.push(`FN:${line},${name}`);
|
||||
let fnh = 0;
|
||||
for (const [name, { hits }] of fnEntries) {
|
||||
out.push(`FNDA:${hits},${name}`);
|
||||
if (hits > 0) fnh++;
|
||||
}
|
||||
out.push(`FNF:${fns.size}`, `FNH:${fnh}`);
|
||||
let lh = 0;
|
||||
for (const [line, hits] of [...lines.entries()].sort((a, b) => a[0] - b[0])) {
|
||||
out.push(`DA:${line},${hits}`);
|
||||
if (hits > 0) lh++;
|
||||
}
|
||||
out.push(`LF:${lines.size}`, `LH:${lh}`, 'end_of_record');
|
||||
}
|
||||
return out.join('\n') + (out.length ? '\n' : '');
|
||||
}
|
||||
|
||||
function buildImpactMap(funcToSpecs: Map<string, Set<string>>): ImpactMap {
|
||||
const map: ImpactMap = {};
|
||||
for (const [key, specs] of funcToSpecs) {
|
||||
const hash = key.lastIndexOf('#');
|
||||
const file = key.slice(0, hash);
|
||||
const line = key.slice(hash + 1);
|
||||
(map[file] ??= {})[line] = [...specs].sort();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Intern spec paths into an index — the lossless on-disk form (see {@link InternedImpactMap}). */
|
||||
export function encodeImpactMap(map: ImpactMap): InternedImpactMap {
|
||||
const index = new Map<string, number>();
|
||||
const specs: string[] = [];
|
||||
const idOf = (s: string): number => {
|
||||
let i = index.get(s);
|
||||
if (i === undefined) index.set(s, (i = specs.push(s) - 1));
|
||||
return i;
|
||||
};
|
||||
const files: InternedImpactMap['files'] = {};
|
||||
for (const file of Object.keys(map)) {
|
||||
files[file] = {};
|
||||
for (const line of Object.keys(map[file])) files[file][line] = map[file][line].map(idOf);
|
||||
}
|
||||
return { specs, files };
|
||||
}
|
||||
|
||||
/** Expand an {@link InternedImpactMap} back to a full {@link ImpactMap}. */
|
||||
export function decodeImpactMap(interned: InternedImpactMap): ImpactMap {
|
||||
const { specs, files } = interned;
|
||||
const map: ImpactMap = {};
|
||||
for (const file of Object.keys(files)) {
|
||||
map[file] = {};
|
||||
for (const line of Object.keys(files[file]))
|
||||
map[file][line] = files[file][line].map((i) => specs[i]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export interface ChangedFile {
|
||||
file: string;
|
||||
/** Changed source line numbers. Omit for file-level (whole-file) resolution. */
|
||||
lines?: number[];
|
||||
}
|
||||
|
||||
export interface ResolveResult {
|
||||
/** Specs that must run. In 'broad' mode this is `allSpecs` if supplied. */
|
||||
specs: string[];
|
||||
/** Changed files absent from the map (new/renamed/never-covered) → force broad. */
|
||||
unmapped: string[];
|
||||
mode: 'scoped' | 'broad';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve changed files to the specs that must run.
|
||||
*
|
||||
* - A file in the map with changed `lines` → specs for the functions those lines
|
||||
* fall into (a line belongs to the function with the greatest start ≤ line).
|
||||
* Lines above the first function (imports / module top-level) can't be pinned
|
||||
* to one function, so they conservatively select ALL specs covering the file.
|
||||
* - A file in the map without `lines` → all specs covering the file.
|
||||
* - A file NOT in the map → `unmapped` → mode 'broad' (run everything). This is
|
||||
* the safety valve that keeps selection sound for new/never-covered code.
|
||||
*
|
||||
* SEAM CONTRACT (line precision): the map only records EXECUTED functions, so it
|
||||
* cannot see an un-executed function sitting between two covered ones. A changed
|
||||
* line inside such a gap is attributed to the nearest preceding covered function
|
||||
* — which can UNDER-SELECT if that region is in truth covered by other specs not
|
||||
* in this (possibly partial/stale) map. Line precision is therefore sound only
|
||||
* when the map is known function-complete for the file. The default-safe usage —
|
||||
* and what the CLI does — is FILE-LEVEL (omit `lines`): a changed file selects
|
||||
* every spec covering any part of it. Treat `lines` as an opt-in optimisation.
|
||||
*/
|
||||
export function resolveImpact(
|
||||
changed: ChangedFile[],
|
||||
map: ImpactMap,
|
||||
opts: { allSpecs?: string[] } = {},
|
||||
): ResolveResult {
|
||||
const specs = new Set<string>();
|
||||
const unmapped: string[] = [];
|
||||
|
||||
for (const { file, lines } of changed) {
|
||||
const fileMap = map[file];
|
||||
if (!fileMap) {
|
||||
unmapped.push(file);
|
||||
continue;
|
||||
}
|
||||
const fnStarts = Object.keys(fileMap)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
const addFile = () => {
|
||||
for (const fnLine of fnStarts) for (const s of fileMap[String(fnLine)]) specs.add(s);
|
||||
};
|
||||
if (!lines || lines.length === 0) {
|
||||
addFile();
|
||||
continue;
|
||||
}
|
||||
for (const line of lines) {
|
||||
// The function containing `line` is the one with the greatest start ≤ line.
|
||||
let owner = -1;
|
||||
for (const start of fnStarts) {
|
||||
if (start <= line) owner = start;
|
||||
else break;
|
||||
}
|
||||
if (owner === -1) {
|
||||
// Above the first function (top-level/import change) → whole file.
|
||||
addFile();
|
||||
break;
|
||||
}
|
||||
for (const s of fileMap[String(owner)]) specs.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
const mode = unmapped.length > 0 ? 'broad' : 'scoped';
|
||||
if (mode === 'broad' && opts.allSpecs) {
|
||||
for (const s of opts.allSpecs) specs.add(s);
|
||||
}
|
||||
return { specs: [...specs].sort(), unmapped, mode };
|
||||
}
|
||||
|
|
@ -160,6 +160,15 @@ describe('TcrExecutor', () => {
|
|||
'export const test = { name: "staged feature test" };\n',
|
||||
);
|
||||
|
||||
// A real `tsc` isn't resolvable in this ephemeral temp dir (no
|
||||
// node_modules), and this test exercises affected-test detection, not
|
||||
// typechecking — stub the typecheck step to a deterministic pass so
|
||||
// run() reaches findAffectedTests instead of bailing at typecheck.
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'package.json'),
|
||||
JSON.stringify({ name: 'tcr-test', scripts: { typecheck: 'node -e ""' } }),
|
||||
);
|
||||
|
||||
// Stage the new file
|
||||
execSync('git add -A', { cwd: tempDir, stdio: 'pipe' });
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ export default defineConfig({
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
// Many suites here are integration tests that spin up a real git repo and
|
||||
// a ts-morph Project per case — ~ms locally but 6–15s on loaded CI runners,
|
||||
// past vitest's 5s default. Raise the package-wide timeout so these don't
|
||||
// flake; fast unit tests finish well under it regardless.
|
||||
testTimeout: 30_000,
|
||||
hookTimeout: 30_000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
|
|
|
|||
57
packages/testing/playwright/coverage-options.ts
Normal file
57
packages/testing/playwright/coverage-options.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { CoverageReportOptions } from 'monocart-coverage-reports';
|
||||
|
||||
/**
|
||||
* Frontend E2E coverage via browser-native V8 (Playwright `page.coverage`),
|
||||
* converted to lcov/html by monocart-coverage-reports. Replaces the previous
|
||||
* istanbul build-time instrumentation (`vite-plugin-istanbul`) — no special
|
||||
* build is needed, V8 collects from the normal bundle at runtime.
|
||||
*
|
||||
* Gated on COVERAGE_ENABLED so normal test runs pay no collection cost.
|
||||
*/
|
||||
export const COVERAGE_ENABLED = process.env.COVERAGE_ENABLED === 'true';
|
||||
|
||||
/**
|
||||
* Shared by the per-test collector (`add`) and the report generator
|
||||
* (`generate`) so both read/write the same outputDir + raw cache.
|
||||
*/
|
||||
export const coverageOptions: CoverageReportOptions = {
|
||||
name: 'n8n E2E Coverage (V8)',
|
||||
outputDir: './coverage',
|
||||
// 'v8' = interactive HTML; 'lcovonly' = lcov.info for Codecov; summary to stdout.
|
||||
reports: ['v8', 'lcovonly', 'console-summary'],
|
||||
// Frontend: keep app bundles served by n8n. Backend: keep n8n's own
|
||||
// packages (the collect step rewrites their urls to repo dist paths).
|
||||
entryFilter: (entry) =>
|
||||
entry.url.includes('/assets/') || /\/packages\/[^/]+(?:\/[^/]+)?\/dist\//.test(entry.url),
|
||||
// Keep first-party application source after source-map expansion; drop deps
|
||||
// and any unmapped dist files. NB nodes-base sources live under `nodes/`,
|
||||
// `credentials/` etc — not `src/` — so don't require `/src/`.
|
||||
sourceFilter: (sourcePath) =>
|
||||
!sourcePath.includes('node_modules') && !sourcePath.includes('/dist/'),
|
||||
// Key Codecov + the impact map on repo-relative `packages/.../src/...`.
|
||||
// Backend map-sources resolve to absolute repo paths (package-qualified);
|
||||
// the frontend bundle resolves relative, so its `src/...` sources have no
|
||||
// package and are attributed to editor-ui.
|
||||
sourcePath: (filePath) => {
|
||||
const norm = filePath.replace(/\\/g, '/');
|
||||
if (norm.startsWith('packages/')) return norm;
|
||||
const i = norm.lastIndexOf('packages/');
|
||||
if (i >= 0) return norm.slice(i);
|
||||
return norm.startsWith('src/') ? `packages/frontend/editor-ui/${norm}` : `packages/${norm}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Directory for per-test (per-spec) raw coverage, used to build the impact map.
|
||||
*
|
||||
* It MUST be a sibling of `outputDir`, never inside it: the shard report's
|
||||
* `CoverageReport.generate()` cleans its own `outputDir` (deleting every child
|
||||
* except `.cache`/the V8 dir), so per-spec raw kept under `outputDir` is wiped
|
||||
* before the emitter reads it. Single source of truth for the fixture (writes),
|
||||
* the emitter (reads), and the drift guard test. See coverage-pipeline.test.ts.
|
||||
*/
|
||||
export function bySpecDir(outputDir: string = coverageOptions.outputDir ?? './coverage'): string {
|
||||
return `${outputDir.replace(/\/+$/, '')}-by-spec`;
|
||||
}
|
||||
|
||||
export const BY_SPEC_DIR = bySpecDir();
|
||||
|
|
@ -3,9 +3,8 @@ import type { CurrentsConfig } from '@currents/playwright';
|
|||
const config: CurrentsConfig = {
|
||||
recordKey: process.env.CURRENTS_RECORD_KEY ?? '',
|
||||
projectId: process.env.CURRENTS_PROJECT_ID ?? 'LRxcNt',
|
||||
coverage: {
|
||||
projects: ['coverage'],
|
||||
},
|
||||
// Coverage is collected via browser-native V8 (fixtures/v8-coverage.ts +
|
||||
// monocart-coverage-reports), not Currents' istanbul fixture.
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import-x/no-default-export
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
type QuarantineTestFixtures,
|
||||
type QuarantineWorkerFixtures,
|
||||
} from '../fixtures/quarantine';
|
||||
import { v8CoverageFixtures } from '../fixtures/v8-coverage';
|
||||
import { n8nPage } from '../pages/n8nPage';
|
||||
import { ApiHelpers } from '../services/api-helper';
|
||||
import { TestError, type TestRequirements } from '../Types';
|
||||
|
|
@ -79,7 +80,7 @@ export const test = base.extend<
|
|||
WorkerFixtures & CurrentsWorkerFixtures & QuarantineWorkerFixtures
|
||||
>({
|
||||
...currentsFixtures.baseFixtures,
|
||||
...currentsFixtures.coverageFixtures,
|
||||
...v8CoverageFixtures,
|
||||
...currentsFixtures.actionFixtures,
|
||||
...observabilityFixtures,
|
||||
...consoleErrorFixtures,
|
||||
|
|
@ -113,6 +114,9 @@ export const test = base.extend<
|
|||
E2E_TESTS: 'true',
|
||||
N8N_RESTRICT_FILE_ACCESS_TO: '',
|
||||
},
|
||||
// Coverage pipeline opt-in: when the coverage runner sets N8N_COVERAGE_DIR,
|
||||
// bridge it to the stack's typed config so containers collect V8 coverage.
|
||||
...(process.env.N8N_COVERAGE_DIR ? { coverageHostDir: process.env.N8N_COVERAGE_DIR } : {}),
|
||||
};
|
||||
|
||||
await use(config);
|
||||
|
|
|
|||
89
packages/testing/playwright/fixtures/v8-coverage.ts
Normal file
89
packages/testing/playwright/fixtures/v8-coverage.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { BrowserContext, Page, TestInfo } from '@playwright/test';
|
||||
import { CoverageReport } from 'monocart-coverage-reports';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
|
||||
import { BY_SPEC_DIR, coverageOptions, COVERAGE_ENABLED } from '../coverage-options';
|
||||
|
||||
/** Spec id = project-relative path (e.g. tests/e2e/nodes/if-node.spec.ts) — the
|
||||
* same id the runner uses, so the impact map keys match runnable specs. */
|
||||
function specId(testInfo: TestInfo): string {
|
||||
return relative(process.cwd(), testInfo.file).split('\\').join('/');
|
||||
}
|
||||
|
||||
const slugify = (spec: string) => spec.replace(/[^a-zA-Z0-9]+/g, '_');
|
||||
|
||||
/**
|
||||
* Browser-native V8 coverage collection (Chromium `page.coverage`), replacing
|
||||
* the previous istanbul-instrumented build + Currents istanbul fixture.
|
||||
*
|
||||
* Wraps the BrowserContext: starts JS coverage on every page at creation
|
||||
* (before navigation, so the initial bundle load is captured) and, on test
|
||||
* teardown, hands each page's V8 coverage to monocart-coverage-reports.
|
||||
*
|
||||
* Coverage is accumulated TWICE: into the shared outputDir (the full shard
|
||||
* report, frontend + backend) and into a PER-SPEC dir keyed by the spec file
|
||||
* (`.by-spec/<slug>/`, with a `.spec` marker naming the spec). The per-spec
|
||||
* data is what lets the impact map say "this spec exercised this function" for
|
||||
* test selection. The extra cost is a second `add()` of data already in memory.
|
||||
*
|
||||
* No-op unless COVERAGE_ENABLED — normal runs pay nothing.
|
||||
*/
|
||||
export const v8CoverageFixtures = {
|
||||
context: async (
|
||||
{ context }: { context: BrowserContext },
|
||||
use: (context: BrowserContext) => Promise<void>,
|
||||
testInfo: TestInfo,
|
||||
) => {
|
||||
if (!COVERAGE_ENABLED) {
|
||||
await use(context);
|
||||
return;
|
||||
}
|
||||
|
||||
const tracked = new Set<Page>();
|
||||
const startCoverage = async (page: Page) => {
|
||||
try {
|
||||
await page.coverage.startJSCoverage({ resetOnNavigation: false });
|
||||
tracked.add(page);
|
||||
} catch {
|
||||
// Non-Chromium browser or coverage unavailable — skip silently.
|
||||
}
|
||||
};
|
||||
|
||||
context.on('page', (page) => {
|
||||
startCoverage(page).catch(() => {});
|
||||
});
|
||||
await Promise.all(context.pages().map(startCoverage));
|
||||
|
||||
await use(context);
|
||||
|
||||
const sharedReport = new CoverageReport(coverageOptions);
|
||||
// Capture this test's RAW page.coverage for the per-spec map, cloning each
|
||||
// entry — MCR's add() mutates input in place, so the same object can't be
|
||||
// fed to two reports. The emitter does the MCR work later from this raw.
|
||||
const perSpecRaw: unknown[] = [];
|
||||
for (const page of tracked) {
|
||||
if (page.isClosed()) continue;
|
||||
try {
|
||||
const coverage = await page.coverage.stopJSCoverage();
|
||||
if (coverage?.length) {
|
||||
perSpecRaw.push(...coverage.map((entry) => structuredClone(entry)));
|
||||
await sharedReport.add(coverage);
|
||||
}
|
||||
} catch {
|
||||
// Page closed before collection — ignore.
|
||||
}
|
||||
}
|
||||
if (perSpecRaw.length) {
|
||||
const spec = specId(testInfo);
|
||||
const specDir = join(BY_SPEC_DIR, slugify(spec));
|
||||
mkdirSync(specDir, { recursive: true });
|
||||
// Unique per test so multiple tests in one spec file accumulate (don't clobber).
|
||||
writeFileSync(
|
||||
join(specDir, `raw-${slugify(testInfo.testId)}.json`),
|
||||
JSON.stringify(perSpecRaw),
|
||||
);
|
||||
writeFileSync(join(specDir, '.spec'), spec);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
const config = {
|
||||
reporter: ['html'],
|
||||
reportDir: 'coverage',
|
||||
tempDir: '.nyc_output',
|
||||
include: [
|
||||
'../../../packages/frontend/editor-ui/src/**/*.{js,ts,vue}',
|
||||
'../../../packages/frontend/editor-ui/dist/**/*.{js,ts}',
|
||||
],
|
||||
exclude: [
|
||||
'**/*.test.{js,ts}',
|
||||
'**/*.spec.{js,ts}',
|
||||
'**/node_modules/**',
|
||||
'**/coverage/**',
|
||||
'**/.nyc_output/**',
|
||||
],
|
||||
sourceMap: true,
|
||||
};
|
||||
|
||||
export = config;
|
||||
|
|
@ -18,15 +18,18 @@
|
|||
"test:container:queue": "playwright test --project='queue:*'",
|
||||
"test:container:multi-main": "playwright test --project='multi-main:*'",
|
||||
"test:container:multi-main:e2e": "playwright test --project='multi-main:e2e'",
|
||||
"test:container:coverage": "playwright test --project=coverage",
|
||||
"test:container:coverage": "COVERAGE_ENABLED=true N8N_COVERAGE_DIR=\"$PWD/coverage/.backend-v8\" playwright test --project=coverage",
|
||||
"test:workflows:setup": "tsx ./tests/cli-workflows/setup-workflow-tests.ts",
|
||||
"test:workflows": "playwright test --project=cli-workflows",
|
||||
"test:workflows:schema": "SCHEMA=true playwright test --project=cli-workflows",
|
||||
"test:workflows:update": "playwright test --project=cli-workflows --update-snapshots",
|
||||
"test:evals": "playwright test --project=eval",
|
||||
"test:evals:smoke": "playwright test --project=eval:smoke",
|
||||
"coverage:report": "node scripts/generate-coverage-report.js",
|
||||
"coverage:clean": "rm -rf coverage .nyc_output",
|
||||
"test:unit": "vitest run",
|
||||
"coverage:shard": "node scripts/run-coverage-shard.mjs",
|
||||
"coverage:emit-shard": "tsx scripts/emit-shard-coverage.ts",
|
||||
"coverage:emit-spec-lcovs": "tsx scripts/emit-spec-lcovs.ts",
|
||||
"coverage:clean": "rm -rf coverage coverage-by-spec .v8-coverage .nyc_output",
|
||||
"coverage:analyse": "node scripts/coverage-analysis.mjs",
|
||||
"install-browsers": "PLAYWRIGHT_BROWSERS_PATH=./.playwright-browsers playwright install chromium --with-deps",
|
||||
"browsers:uninstall": "playwright uninstall --all",
|
||||
|
|
@ -69,10 +72,11 @@
|
|||
"n8n-core": "workspace:*",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"nanoid": "catalog:",
|
||||
"nyc": "^17.1.0",
|
||||
"monocart-coverage-reports": "^2.12.0",
|
||||
"otplib": "^12.0.1",
|
||||
"ts-morph": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"zod": "catalog:"
|
||||
"zod": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
packages/testing/playwright/scripts/aggregate-coverage.mjs
Normal file
82
packages/testing/playwright/scripts/aggregate-coverage.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Aggregate downloaded shard artifacts into (1) a unified coverage report lcov
|
||||
* and (2) the per-spec impact map, using the property-tested janitor
|
||||
* `merge-coverage`. Replaces brittle inline `find`/`cp` bash in the workflows —
|
||||
* the janitor CLI path lives here, in one place, instead of per-YAML-string.
|
||||
*
|
||||
* node aggregate-coverage.mjs --shards=<dir> --out=<dir>
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
|
||||
|
||||
const arg = (name, fallback) =>
|
||||
process.argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3) ?? fallback;
|
||||
|
||||
const SHARDS = arg('shards', '/tmp/shards');
|
||||
const OUT = arg('out', './coverage');
|
||||
|
||||
/** Recursively collect files under `dir` matching `predicate(filename)`. */
|
||||
function findFiles(dir, predicate, acc = []) {
|
||||
if (!existsSync(dir)) return acc;
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = path.join(dir, entry);
|
||||
if (statSync(p).isDirectory()) findFiles(p, predicate, acc);
|
||||
else if (predicate(entry, p)) acc.push(p);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** Stage matching files into a fresh temp dir with unique names, return the dir. */
|
||||
function stage(label, files) {
|
||||
const dir = path.join('/tmp', `agg-${label}`);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
files.forEach((f, i) => copyFileSync(f, path.join(dir, `${label}-${i}.lcov`)));
|
||||
return dir;
|
||||
}
|
||||
|
||||
function merge(inputsDir, outLcov, outMap) {
|
||||
execFileSync(
|
||||
'node',
|
||||
[
|
||||
JANITOR_CLI,
|
||||
'merge-coverage',
|
||||
`--inputs-dir=${inputsDir}`,
|
||||
`--out-lcov=${outLcov}`,
|
||||
`--out-map=${outMap}`,
|
||||
],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
// 1. Report: shard-level lcovs (frontend + backend) → unified lcov for Codecov.
|
||||
const shardLcovs = findFiles(SHARDS, (name) => name === 'lcov.info');
|
||||
console.log(`Report: ${shardLcovs.length} shard lcov(s)`);
|
||||
if (shardLcovs.length) {
|
||||
merge(stage('report', shardLcovs), path.join(OUT, 'lcov.info'), '/tmp/agg-report-map.json');
|
||||
} else {
|
||||
console.warn(' ⚠ no shard lcov.info found — skipping report');
|
||||
}
|
||||
|
||||
// 2. Impact map: per-spec frontend lcovs (TN-tagged) → spec-keyed map for selection.
|
||||
const specLcovs = findFiles(
|
||||
SHARDS,
|
||||
(name, p) => name.endsWith('.lcov') && p.includes(`${path.sep}by-spec${path.sep}`),
|
||||
);
|
||||
console.log(`Impact map: ${specLcovs.length} per-spec lcov(s)`);
|
||||
if (specLcovs.length) {
|
||||
merge(stage('spec', specLcovs), '/tmp/agg-spec-fe.lcov', path.join(OUT, 'impact-map.json'));
|
||||
} else {
|
||||
console.warn(' ⚠ no per-spec lcovs found — impact map not built');
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
import { CoverageReport } from 'monocart-coverage-reports';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BY_SPEC_DIR, bySpecDir, coverageOptions } from '../coverage-options';
|
||||
|
||||
/** Is `child` the same as, or nested under, `parent`? */
|
||||
const isUnder = (child: string, parent: string): boolean => {
|
||||
const c = resolve(child);
|
||||
const p = resolve(parent);
|
||||
return c === p || c.startsWith(`${p}/`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Regression guard for the per-spec coverage seam (DEVP-205).
|
||||
*
|
||||
* Bug: the fixture wrote per-spec raw under `./coverage/.by-spec`, but the shard
|
||||
* report's `CoverageReport.generate()` cleans its `outputDir` (deleting every
|
||||
* child except `.cache`/the V8 dir) — so the per-spec raw was wiped before the
|
||||
* emitter read it, and the impact map came out empty. The fix puts per-spec raw
|
||||
* in a SIBLING of `outputDir`. These tests fail if it ever drifts back inside.
|
||||
*/
|
||||
describe('per-spec coverage dir does not drift under the report outputDir', () => {
|
||||
it('bySpecDir is OUTSIDE the report outputDir for any outputDir', () => {
|
||||
expect(isUnder(BY_SPEC_DIR, coverageOptions.outputDir ?? './coverage')).toBe(false);
|
||||
for (const out of ['./coverage', '/a/b/coverage', 'cov/', './nested/cov']) {
|
||||
expect(isUnder(bySpecDir(out), out)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("the shard report's generate() does NOT delete the per-spec sibling dir", async () => {
|
||||
const tmp = resolve('/tmp', `cov-drift-${process.pid}`);
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
const outputDir = join(tmp, 'coverage');
|
||||
const sibling = bySpecDir(outputDir);
|
||||
mkdirSync(join(sibling, 'specA'), { recursive: true });
|
||||
writeFileSync(join(sibling, 'specA', '.spec'), 'tests/e2e/x.spec.ts');
|
||||
|
||||
// Minimal real coverage so generate() produces a report and runs its clean
|
||||
// (the clean is what deleted the per-spec dir when it lived under outputDir).
|
||||
const report = new CoverageReport({ ...coverageOptions, outputDir });
|
||||
await report.add([
|
||||
{
|
||||
url: 'http://host/assets/a.js',
|
||||
source: 'function f(){ return 1; }\nf();\n',
|
||||
functions: [
|
||||
{
|
||||
functionName: 'f',
|
||||
isBlockCoverage: true,
|
||||
ranges: [{ startOffset: 0, endOffset: 30, count: 1 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as never);
|
||||
await report.generate();
|
||||
|
||||
expect(existsSync(join(sibling, 'specA', '.spec'))).toBe(true);
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -24,6 +24,13 @@ const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.
|
|||
const PLAYWRIGHT_PREFIX = path.relative(REPO_ROOT, PLAYWRIGHT_DIR) + path.sep;
|
||||
const CONTAINER_STARTUP_TIME = 22_500; // 22.5s average per fixture
|
||||
|
||||
// Specs excluded from orchestrated distribution. Under coverage instrumentation
|
||||
// (no container reuse + slower execution), high-flake specs balloon into
|
||||
// multi-minute retry tails that tip a single shard over its timeout. Quarantine
|
||||
// them here until the flakiness is fixed via the Flaky pipeline.
|
||||
// tests/e2e/ai/hitl-for-tools.spec.ts — 34.2% flakyRate (2x next worst)
|
||||
const QUARANTINE = new Set(['tests/e2e/ai/hitl-for-tools.spec.ts']);
|
||||
|
||||
const CAPABILITY_IMAGES = {
|
||||
email: ['mailpit'],
|
||||
kafka: ['kafka'],
|
||||
|
|
@ -135,7 +142,7 @@ if (matrixMode) {
|
|||
|
||||
const matrix = result.shards.map((shard) => ({
|
||||
shard: shard.shard,
|
||||
specs: shard.specs.join(' '),
|
||||
specs: shard.specs.filter((s) => !QUARANTINE.has(s)).join(' '),
|
||||
images: getRequiredImages(shard.capabilities).join(' '),
|
||||
}));
|
||||
console.log(JSON.stringify(matrix));
|
||||
|
|
@ -150,6 +157,6 @@ if (matrixMode) {
|
|||
const result = getOrchestration(shards, { impact: impactMode, files: filesArg, base: baseArg });
|
||||
const shard = result.shards[index];
|
||||
if (shard) {
|
||||
console.log(shard.specs.join('\n'));
|
||||
console.log(shard.specs.filter((s) => !QUARANTINE.has(s)).join('\n'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
185
packages/testing/playwright/scripts/emit-shard-coverage.ts
Normal file
185
packages/testing/playwright/scripts/emit-shard-coverage.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* Shard-side coverage emitter (shard-side compaction, DEVP-205).
|
||||
*
|
||||
* Resolves THIS shard's own raw V8 to a small per-shard lcov, so shards upload
|
||||
* ~0.4MB of lcov instead of ~5GB of raw, and the aggregate merges lcovs in
|
||||
* bounded memory instead of OOMing on the full-suite raw merge.
|
||||
*
|
||||
* - Frontend: browser V8 already in outputDir/.cache (inline maps w/ sources) —
|
||||
* monocart resolves it with no extra inputs.
|
||||
* - Backend: Node V8 from the containers (N8N_COVERAGE_DIR). The repo isn't
|
||||
* built on the shard, so .js/.map BYTES are read from the n8n image's dist
|
||||
* (docker cp'd to IMAGE_DIST_ROOT — the exact executed files), while the map's
|
||||
* `sources` are resolved to the checkout's `packages/<x>/src/*.ts`. No build.
|
||||
*
|
||||
* Per-shard volume (~1/8 of the suite) fits in memory, so MCR's generate() is
|
||||
* safe here; the OOM only happened when merging all shards at once.
|
||||
*/
|
||||
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { CoverageReport } from 'monocart-coverage-reports';
|
||||
|
||||
import { coverageOptions } from '../coverage-options';
|
||||
|
||||
const REPO_ROOT = resolve(process.cwd(), '../../..');
|
||||
// Where `docker cp` placed the image's n8n package tree, and the in-image root
|
||||
// those urls are prefixed with (see docker/images/n8n/Dockerfile).
|
||||
const IMAGE_DIST_ROOT = process.env.IMAGE_DIST_ROOT;
|
||||
const IMAGE_ROOT_PREFIX = process.env.IMAGE_ROOT_PREFIX ?? '/usr/local/lib/node_modules/n8n';
|
||||
|
||||
/** Map workspace package name → repo dir (e.g. n8n-core → core, n8n → cli). */
|
||||
function buildPackageMap(): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
const roots = [
|
||||
join(REPO_ROOT, 'packages'),
|
||||
join(REPO_ROOT, 'packages/@n8n'),
|
||||
join(REPO_ROOT, 'packages/frontend'),
|
||||
];
|
||||
for (const root of roots) {
|
||||
if (!existsSync(root)) continue;
|
||||
for (const entry of readdirSync(root)) {
|
||||
const pkgJson = join(root, entry, 'package.json');
|
||||
if (!existsSync(pkgJson)) continue;
|
||||
try {
|
||||
const name = JSON.parse(readFileSync(pkgJson, 'utf8')).name as string;
|
||||
if (name) map.set(name, join(root, entry).slice(REPO_ROOT.length + 1));
|
||||
} catch {
|
||||
// unreadable/ malformed package.json — skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function listJsonFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const e of readdirSync(dir)) {
|
||||
const p = join(dir, e);
|
||||
if (statSync(p).isDirectory()) out.push(...listJsonFiles(p));
|
||||
else if (e.endsWith('.json')) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A resolved backend entry: where to read .js/.map bytes, and the repo dist
|
||||
* dir its map sources resolve against (→ checkout `src/*.ts`). */
|
||||
function resolveBackendUrl(url: string, pkgMap: Map<string, string>) {
|
||||
const m = url.match(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\/dist\/(.+)$/);
|
||||
if (!m) return null;
|
||||
const repoDir = pkgMap.get(m[1]);
|
||||
if (!repoDir) return null;
|
||||
const repoDistDir = join(REPO_ROOT, repoDir, 'dist');
|
||||
const repoDistFile = join(repoDistDir, m[2]);
|
||||
// Byte source: image dist on the shard (prefix-remap), or repo dist locally.
|
||||
const bytesFile = IMAGE_DIST_ROOT
|
||||
? url.replace(/^file:\/\//, '').replace(IMAGE_ROOT_PREFIX, IMAGE_DIST_ROOT)
|
||||
: repoDistFile;
|
||||
return { repoDistFile, bytesFile };
|
||||
}
|
||||
|
||||
const stats = { entries: 0, noMatch: 0, noPkg: 0, noJs: 0, noMap: 0, ok: 0 };
|
||||
|
||||
async function addBackendCoverage(report: CoverageReport): Promise<number> {
|
||||
const dir = process.env.N8N_COVERAGE_DIR;
|
||||
if (!dir || !existsSync(dir)) return 0;
|
||||
const pkgMap = buildPackageMap();
|
||||
let added = 0;
|
||||
for (const file of listJsonFiles(dir)) {
|
||||
let parsed: { result?: Array<{ url: string }> };
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const entries = (parsed.result ?? [])
|
||||
.map((e) => {
|
||||
stats.entries++;
|
||||
const r = resolveBackendUrl(e.url, pkgMap);
|
||||
if (!r) {
|
||||
if (/\/node_modules\/.+\/dist\//.test(e.url)) stats.noPkg++;
|
||||
else stats.noMatch++;
|
||||
return null;
|
||||
}
|
||||
let source: string;
|
||||
try {
|
||||
source = readFileSync(r.bytesFile, 'utf8');
|
||||
} catch {
|
||||
stats.noJs++;
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const map = JSON.parse(readFileSync(`${r.bytesFile}.map`, 'utf8')) as {
|
||||
sources?: string[];
|
||||
};
|
||||
// Resolve map sources to the checkout's repo src/*.ts (absolute).
|
||||
// Sources are relative to the .js.map's OWN dir (e.g. a file at
|
||||
// dist/commands/x.js.map has `../../src/commands/x.ts`), so anchor
|
||||
// at the dist file's dir — not the package dist root.
|
||||
map.sources = (map.sources ?? []).map((s) => resolve(dirname(r.repoDistFile), s));
|
||||
const b64 = Buffer.from(JSON.stringify(map)).toString('base64');
|
||||
source =
|
||||
source.replace(/\n?\/\/# sourceMappingURL=.*\s*$/, '\n') +
|
||||
`//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}\n`;
|
||||
} catch {
|
||||
stats.noMap++;
|
||||
return null;
|
||||
}
|
||||
stats.ok++;
|
||||
// Key on the repo dist path so entryFilter (/packages/.../dist/) matches.
|
||||
return { ...e, url: pathToFileURL(r.repoDistFile).href, source };
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (entries.length) {
|
||||
await report.add(entries as never);
|
||||
added += entries.length;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/** Drop raw cache files truncated mid-write (a killed test/container) so MCR's
|
||||
* generate() — which hard-aborts on the first unparseable file — survives. */
|
||||
function pruneCorruptRaw(dir: string): number {
|
||||
if (!existsSync(dir)) return 0;
|
||||
let dropped = 0;
|
||||
for (const file of listJsonFiles(dir)) {
|
||||
try {
|
||||
JSON.parse(readFileSync(file, 'utf8'));
|
||||
} catch {
|
||||
unlinkSync(file);
|
||||
dropped++;
|
||||
}
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 Emitting per-shard coverage lcov...');
|
||||
if (IMAGE_DIST_ROOT) console.log(` image dist: ${IMAGE_DIST_ROOT}`);
|
||||
const report = new CoverageReport(coverageOptions);
|
||||
|
||||
const dropped = pruneCorruptRaw(join(coverageOptions.outputDir ?? './coverage', '.cache'));
|
||||
if (dropped) console.warn(` ⚠ dropped ${dropped} corrupt raw coverage file(s)`);
|
||||
|
||||
const backend = await addBackendCoverage(report);
|
||||
console.log(
|
||||
` backend entries: ${stats.entries} seen → ${stats.ok} resolved ` +
|
||||
`(noPkg ${stats.noPkg}, noJs ${stats.noJs}, noMap ${stats.noMap}, other ${stats.noMatch})`,
|
||||
);
|
||||
|
||||
const result = await report.generate();
|
||||
if (!result || !result.files?.length) {
|
||||
console.error('❌ No coverage data resolved for this shard.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`✅ Per-shard lcov written (${result.files.length} files, ${backend} backend entries)`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
69
packages/testing/playwright/scripts/emit-spec-lcovs.ts
Normal file
69
packages/testing/playwright/scripts/emit-spec-lcovs.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Resolve per-spec frontend coverage (raw page.coverage the v8-coverage fixture
|
||||
* wrote under BY_SPEC_DIR) into one lcov per spec, tagged with the spec id in
|
||||
* `TN:`. These feed the impact map, letting a git diff select the E2E specs that
|
||||
* exercise the touched frontend code.
|
||||
*
|
||||
* Frontend only: backend coverage is a shared worker-scoped process with no
|
||||
* per-test boundary, so it stays at report granularity. See DEVP-205.
|
||||
*/
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { CoverageReport } from 'monocart-coverage-reports';
|
||||
|
||||
import { BY_SPEC_DIR, coverageOptions } from '../coverage-options';
|
||||
|
||||
const OUT_DIR = join(coverageOptions.outputDir ?? './coverage', 'by-spec');
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(BY_SPEC_DIR)) {
|
||||
console.log(`emit-spec-lcovs: ${BY_SPEC_DIR} absent — no per-spec coverage collected`);
|
||||
return;
|
||||
}
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
const dirs = readdirSync(BY_SPEC_DIR).filter((d) => statSync(join(BY_SPEC_DIR, d)).isDirectory());
|
||||
let withMarker = 0;
|
||||
let withRaw = 0;
|
||||
let emitted = 0;
|
||||
for (const slug of dirs) {
|
||||
const dir = join(BY_SPEC_DIR, slug);
|
||||
const specMarker = join(dir, '.spec');
|
||||
if (!existsSync(specMarker)) continue;
|
||||
withMarker++;
|
||||
const spec = readFileSync(specMarker, 'utf8').trim();
|
||||
const rawFiles = readdirSync(dir).filter((f) => f.startsWith('raw-') && f.endsWith('.json'));
|
||||
if (!rawFiles.length) continue;
|
||||
withRaw++;
|
||||
const report = new CoverageReport({
|
||||
...coverageOptions,
|
||||
name: spec,
|
||||
outputDir: dir,
|
||||
reports: ['lcovonly'],
|
||||
});
|
||||
for (const rf of rawFiles) {
|
||||
try {
|
||||
await report.add(JSON.parse(readFileSync(join(dir, rf), 'utf8')));
|
||||
} catch (error) {
|
||||
console.warn(` ⚠ ${slug}/${rf}: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
const result = await report.generate();
|
||||
const lcovPath = join(dir, 'lcov.info');
|
||||
if (!result || !result.files?.length || !existsSync(lcovPath)) continue;
|
||||
// Force every record's TN to the real spec id so the merge attributes it.
|
||||
let lcov = readFileSync(lcovPath, 'utf8').replace(/^TN:.*$/gm, `TN:${spec}`);
|
||||
if (!lcov.startsWith('TN:')) lcov = `TN:${spec}\n${lcov}`;
|
||||
writeFileSync(join(OUT_DIR, `${slug}.lcov`), lcov);
|
||||
emitted++;
|
||||
}
|
||||
console.log(
|
||||
`emit-spec-lcovs: ${dirs.length} dirs, ${withMarker} with .spec, ${withRaw} with raw → ` +
|
||||
`${emitted} per-spec lcov(s) in ${OUT_DIR}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Simple script to merge coverage reports and generate HTML output
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const NYC_OUTPUT_DIR = path.join(__dirname, '..', '.nyc_output');
|
||||
const COVERAGE_DIR = path.join(__dirname, '..', 'coverage');
|
||||
const NYC_CONFIG = path.join(__dirname, '..', 'nyc.config.ts');
|
||||
const COVERAGE_INPUT_DIR = path.join(NYC_OUTPUT_DIR, 'coverage');
|
||||
|
||||
function main() {
|
||||
console.log('🔍 Generating Coverage Report');
|
||||
console.log('==============================\n');
|
||||
|
||||
const jsonFiles = fs.existsSync(COVERAGE_INPUT_DIR)
|
||||
? fs.readdirSync(COVERAGE_INPUT_DIR).filter((f) => f.endsWith('.json'))
|
||||
: [];
|
||||
|
||||
if (jsonFiles.length === 0) {
|
||||
console.error('❌ No coverage data found in .nyc_output/coverage/');
|
||||
console.log('\nTo generate coverage data:');
|
||||
console.log(
|
||||
'1. Build editor-ui with coverage: BUILD_WITH_COVERAGE=true pnpm --filter n8n-editor-ui build',
|
||||
);
|
||||
console.log('2. Run Playwright tests with coverage: pnpm test:container:coverage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found ${jsonFiles.length} coverage files in .nyc_output/coverage/\n`);
|
||||
|
||||
try {
|
||||
const mergedFile = path.join(NYC_OUTPUT_DIR, 'out.json');
|
||||
console.log('Merging coverage files...');
|
||||
execSync(`npx nyc merge "${COVERAGE_INPUT_DIR}" "${mergedFile}"`, { stdio: 'inherit' });
|
||||
|
||||
console.log('Generating coverage reports...');
|
||||
execSync(
|
||||
`npx nyc report --reporter=html --reporter=lcov --report-dir=${COVERAGE_DIR} --temp-dir=${NYC_OUTPUT_DIR} --config=${NYC_CONFIG} --exclude-after-remap=false`,
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
|
||||
const htmlReportPath = path.join(COVERAGE_DIR, 'index.html');
|
||||
const lcovPath = path.join(COVERAGE_DIR, 'lcov.info');
|
||||
|
||||
console.log(`\n✅ Coverage reports generated successfully!`);
|
||||
if (fs.existsSync(htmlReportPath)) {
|
||||
console.log(`📊 HTML Report: ${htmlReportPath}`);
|
||||
}
|
||||
if (fs.existsSync(lcovPath)) {
|
||||
console.log(`📄 LCOV Report: ${lcovPath} (for Codecov)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to generate coverage report:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
123
packages/testing/playwright/scripts/impact-map.mjs
Normal file
123
packages/testing/playwright/scripts/impact-map.mjs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Prototype: turn an MCR lcov into a function<->source<->spec impact map, then
|
||||
* resolve a git diff to the E2E spec(s) that exercise the changed code.
|
||||
*
|
||||
* The canary report came from ONE spec (if-node.spec.ts), so all its coverage
|
||||
* is attributable to that spec. In production each per-test lcov carries its
|
||||
* own spec id; here we pass it in. Keyed on FUNCTION coverage (not lines) so
|
||||
* module-load-only execution doesn't over-select.
|
||||
*
|
||||
* node impact-map.mjs <lcov> <spec-id> <diff-file>
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const [lcovPath, specId, diffPath] = process.argv.slice(2);
|
||||
|
||||
// ---- 1. Parse lcov -> per-file covered functions ----------------------------
|
||||
function parseLcov(text) {
|
||||
const files = new Map(); // sf -> [{ name, line, hits }]
|
||||
let sf = null;
|
||||
const fnLine = new Map(); // name -> line (within current file)
|
||||
for (const raw of text.split('\n')) {
|
||||
if (raw.startsWith('SF:')) {
|
||||
sf = raw.slice(3);
|
||||
files.set(sf, []);
|
||||
fnLine.clear();
|
||||
} else if (raw.startsWith('FN:')) {
|
||||
const [line, ...name] = raw.slice(3).split(',');
|
||||
fnLine.set(name.join(','), Number(line));
|
||||
} else if (raw.startsWith('FNDA:')) {
|
||||
const [hits, ...name] = raw.slice(5).split(',');
|
||||
const n = name.join(',');
|
||||
files.get(sf).push({ name: n, line: fnLine.get(n) ?? 0, hits: Number(hits) });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// ---- 2. Build the map (this report == one spec) -----------------------------
|
||||
// fileToSpecs: source -> Set(spec) | funcToSpecs: `${source}#${line}` -> Set(spec)
|
||||
function buildMap(files, spec) {
|
||||
const fileToSpecs = new Map();
|
||||
const funcToSpecs = new Map();
|
||||
const fileFns = new Map(); // source -> [{name,line}] covered, sorted by line
|
||||
for (const [sf, fns] of files) {
|
||||
const covered = fns.filter((f) => f.hits > 0);
|
||||
if (!covered.length) continue;
|
||||
fileToSpecs.set(sf, new Set([spec]));
|
||||
fileFns.set(
|
||||
sf,
|
||||
covered.map((f) => ({ name: f.name, line: f.line })).sort((a, b) => a.line - b.line),
|
||||
);
|
||||
for (const f of covered) {
|
||||
const k = `${sf}#${f.line}`;
|
||||
(funcToSpecs.get(k) ?? funcToSpecs.set(k, new Set()).get(k)).add(spec);
|
||||
}
|
||||
}
|
||||
return { fileToSpecs, funcToSpecs, fileFns };
|
||||
}
|
||||
|
||||
// ---- 3. Parse a unified git diff -> changed files + changed new-side lines ---
|
||||
function parseDiff(text) {
|
||||
const changed = new Map(); // path -> Set(lineNumber)
|
||||
let path = null;
|
||||
let newLine = 0;
|
||||
for (const raw of text.split('\n')) {
|
||||
if (raw.startsWith('+++ ')) {
|
||||
path = raw.slice(4).replace(/^b\//, '').trim();
|
||||
if (!changed.has(path)) changed.set(path, new Set());
|
||||
} else if (raw.startsWith('@@')) {
|
||||
const m = raw.match(/\+(\d+)/);
|
||||
newLine = m ? Number(m[1]) : 0;
|
||||
} else if (path) {
|
||||
if (raw.startsWith('+') && !raw.startsWith('+++')) {
|
||||
changed.get(path).add(newLine);
|
||||
newLine++;
|
||||
} else if (!raw.startsWith('-')) {
|
||||
newLine++; // context line advances the new-side counter
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// A function is "touched" if a changed line falls in [fnLine, nextFnLine).
|
||||
function touchedFns(fileFns, changedLines) {
|
||||
if (!fileFns) return [];
|
||||
const out = [];
|
||||
for (let i = 0; i < fileFns.length; i++) {
|
||||
const start = fileFns[i].line;
|
||||
const end = i + 1 < fileFns.length ? fileFns[i + 1].line : Infinity;
|
||||
if ([...changedLines].some((l) => l >= start && l < end)) out.push(fileFns[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- run --------------------------------------------------------------------
|
||||
const map = buildMap(parseLcov(readFileSync(lcovPath, 'utf8')), specId);
|
||||
const diff = parseDiff(readFileSync(diffPath, 'utf8'));
|
||||
|
||||
console.log(`\nDiff touches ${diff.size} file(s):`);
|
||||
const selected = new Set();
|
||||
for (const [file, lines] of diff) {
|
||||
const fileSpecs = map.fileToSpecs.get(file);
|
||||
if (!fileSpecs) {
|
||||
console.log(` ✗ ${file} — no coverage entry → can't scope (default: run broad)`);
|
||||
continue;
|
||||
}
|
||||
const fns = touchedFns(map.fileFns.get(file), lines);
|
||||
const fnSpecs = new Set();
|
||||
for (const f of fns)
|
||||
for (const s of map.funcToSpecs.get(`${file}#${f.line}`) ?? []) fnSpecs.add(s);
|
||||
const specs = fns.length ? fnSpecs : fileSpecs; // function-precise when we can pin the function
|
||||
for (const s of specs) selected.add(s);
|
||||
console.log(
|
||||
` ✓ ${file}\n changed lines: ${[...lines].slice(0, 8).join(', ')}${lines.size > 8 ? '…' : ''}` +
|
||||
`\n touched covered fns: ${fns.length ? fns.map((f) => `${f.name}@${f.line}`).join(', ') : '(none pinned → file-level)'}` +
|
||||
`\n → specs: ${[...specs].join(', ')}`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`\n=> E2E specs to run: ${selected.size ? [...selected].join(', ') : '(none — skip E2E)'}\n`,
|
||||
);
|
||||
47
packages/testing/playwright/scripts/run-coverage-shard.mjs
Normal file
47
packages/testing/playwright/scripts/run-coverage-shard.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Coverage shard runner — the coverage caller's test-command.
|
||||
*
|
||||
* Runs the Playwright coverage project, then resolves THIS shard's raw V8 to a
|
||||
* small lcov + per-spec lcovs. Keeping this here (not as steps in
|
||||
* test-e2e-reusable.yml) leaves the shared E2E workflow coverage-agnostic: it
|
||||
* just runs the test-command and uploads coverage/, same as any other shard.
|
||||
*
|
||||
* Args after the script (e.g. `--workers=1 <specs>`, appended by the reusable)
|
||||
* are forwarded to Playwright. Coverage resolution is best-effort and never
|
||||
* masks the test exit code.
|
||||
*/
|
||||
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
|
||||
const forwarded = process.argv.slice(2);
|
||||
const sh = (cmd, args, env = {}) =>
|
||||
spawnSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ...env } });
|
||||
|
||||
// 1. Run the coverage project (forward workers/specs from the reusable).
|
||||
const test = sh('pnpm', ['test:container:coverage', ...forwarded]);
|
||||
|
||||
// 2. Resolve coverage — best-effort, even when tests failed (we still want the data).
|
||||
try {
|
||||
const image = process.env.TEST_IMAGE_N8N ?? 'n8nio/n8n:local';
|
||||
// Backend V8 files are written by the container as uid 1000 / mode 0600.
|
||||
spawnSync('sudo', ['chmod', '-R', 'a+rwX', 'coverage/.backend-v8'], { stdio: 'ignore' });
|
||||
// Extract the image's built dist (.js + .js.map) for backend source resolution.
|
||||
const cid = execFileSync('docker', ['create', image]).toString().trim();
|
||||
execFileSync('rm', ['-rf', 'img-dist']);
|
||||
execFileSync('docker', ['cp', `${cid}:/usr/local/lib/node_modules/n8n`, './img-dist']);
|
||||
execFileSync('docker', ['rm', cid], { stdio: 'ignore' });
|
||||
sh('pnpm', ['coverage:emit-shard'], {
|
||||
IMAGE_DIST_ROOT: `${process.cwd()}/img-dist`,
|
||||
N8N_COVERAGE_DIR: `${process.cwd()}/coverage/.backend-v8`,
|
||||
// monocart's generate() is memory-hungry; containers are stopped, RAM is free.
|
||||
NODE_OPTIONS: '--max-old-space-size=12288',
|
||||
});
|
||||
sh('pnpm', ['coverage:emit-spec-lcovs']);
|
||||
} catch (error) {
|
||||
console.error('coverage emit failed (non-fatal):', String(error));
|
||||
}
|
||||
|
||||
process.exit(test.status ?? 1);
|
||||
46
packages/testing/playwright/scripts/select-affected-e2e.mjs
Normal file
46
packages/testing/playwright/scripts/select-affected-e2e.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Resolve changed files → the E2E specs that must run, using the coverage
|
||||
* impact map. Prints the janitor select-e2e JSON ({ specs, unmapped, mode }).
|
||||
*
|
||||
* node select-affected-e2e.mjs <changed-file> [<changed-file> ...]
|
||||
* CHANGED_FILES=a.ts,b.vue node select-affected-e2e.mjs
|
||||
*
|
||||
* MAP SOURCE SEAM — this is the one spot to change when moving off the committed
|
||||
* file. Today the map is the committed snapshot the nightly publishes. To pull
|
||||
* it from a remote webhook instead, replace `resolveMapPath()` with a fetch to a
|
||||
* temp file. Keep it FAIL-OPEN: on ANY failure return null so selection degrades
|
||||
* to BROAD (run everything) — a missing/stale/unreachable map must never cause a
|
||||
* test to be skipped.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
||||
const JANITOR_CLI = path.resolve(__dirname, '..', '..', 'janitor', 'dist', 'cli.js');
|
||||
const COMMITTED_MAP = path.join(REPO_ROOT, '.github', 'test-metrics', 'e2e-impact-map.json');
|
||||
|
||||
/** @returns {string | null} path to a readable impact map, or null → fail-open broad. */
|
||||
function resolveMapPath() {
|
||||
// FUTURE: fetch a remote webhook to a temp file here and return that path;
|
||||
// wrap in try/catch and return null on failure (fail-open).
|
||||
return existsSync(COMMITTED_MAP) ? COMMITTED_MAP : null;
|
||||
}
|
||||
|
||||
const changedFiles = process.argv.slice(2).join(',') || process.env.CHANGED_FILES || '';
|
||||
const mapPath = resolveMapPath();
|
||||
|
||||
const args = ['select-e2e', `--changed-files=${changedFiles}`];
|
||||
// Omitting --map (or pointing at a missing file) makes select-e2e fail open to broad.
|
||||
if (mapPath) args.push(`--map=${mapPath}`);
|
||||
const allSpecs = process.env.ALL_SPECS_FILE;
|
||||
if (allSpecs) args.push(`--all-specs=${allSpecs}`);
|
||||
|
||||
const out = execFileSync('node', [JANITOR_CLI, ...args], { encoding: 'utf-8' });
|
||||
process.stdout.write(out);
|
||||
11
packages/testing/playwright/vitest.config.ts
Normal file
11
packages/testing/playwright/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Unit tests for the coverage pipeline scripts/fixtures only. Scoped tightly so
|
||||
// vitest never picks up the Playwright e2e specs under tests/ (those are *.spec.ts
|
||||
// run by Playwright, not vitest).
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['scripts/**/*.test.ts', 'fixtures/**/*.test.ts', '*.test.ts'],
|
||||
exclude: ['tests/**'],
|
||||
},
|
||||
});
|
||||
429
pnpm-lock.yaml
429
pnpm-lock.yaml
|
|
@ -231,6 +231,9 @@ catalogs:
|
|||
eslint-plugin-oxlint:
|
||||
specifier: ^1.61.0
|
||||
version: 1.61.0
|
||||
fast-check:
|
||||
specifier: ^3.23.2
|
||||
version: 3.23.2
|
||||
fast-glob:
|
||||
specifier: 3.2.12
|
||||
version: 3.2.12
|
||||
|
|
@ -4589,9 +4592,6 @@ importers:
|
|||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
|
||||
vite-plugin-istanbul:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
vite-plugin-node-polyfills:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.0(rollup@4.52.4)(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
|
|
@ -5060,6 +5060,9 @@ importers:
|
|||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(vitest@4.1.1)
|
||||
fast-check:
|
||||
specifier: 'catalog:'
|
||||
version: 3.23.2
|
||||
ts-morph:
|
||||
specifier: 'catalog:'
|
||||
version: 27.0.2
|
||||
|
|
@ -5162,6 +5165,9 @@ importers:
|
|||
mockserver-client:
|
||||
specifier: ^5.15.0
|
||||
version: 5.15.0
|
||||
monocart-coverage-reports:
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.12
|
||||
n8n:
|
||||
specifier: workspace:*
|
||||
version: link:../../cli
|
||||
|
|
@ -5177,9 +5183,6 @@ importers:
|
|||
nanoid:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.8
|
||||
nyc:
|
||||
specifier: ^17.1.0
|
||||
version: 17.1.0
|
||||
otplib:
|
||||
specifier: ^12.0.1
|
||||
version: 12.0.1
|
||||
|
|
@ -5189,6 +5192,9 @@ importers:
|
|||
tsx:
|
||||
specifier: 'catalog:'
|
||||
version: 4.19.3
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
|
||||
zod:
|
||||
specifier: 3.25.67
|
||||
version: 3.25.67
|
||||
|
|
@ -12067,10 +12073,18 @@ packages:
|
|||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
acorn-loose@8.5.2:
|
||||
resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@7.4.1:
|
||||
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
|
@ -12254,10 +12268,6 @@ packages:
|
|||
append-field@1.0.0:
|
||||
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
||||
|
||||
append-transform@2.0.0:
|
||||
resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
aproba@2.0.0:
|
||||
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
|
||||
|
||||
|
|
@ -12269,9 +12279,6 @@ packages:
|
|||
resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
archy@1.0.0:
|
||||
resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==}
|
||||
|
||||
are-we-there-yet@3.0.1:
|
||||
resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -12802,10 +12809,6 @@ packages:
|
|||
cacheable@1.10.3:
|
||||
resolution: {integrity: sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==}
|
||||
|
||||
caching-transform@4.0.0:
|
||||
resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -13079,9 +13082,6 @@ packages:
|
|||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
||||
|
|
@ -13183,6 +13183,10 @@ packages:
|
|||
resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@14.0.3:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
|
|
@ -13278,6 +13282,9 @@ packages:
|
|||
console-control-strings@1.1.0:
|
||||
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||
|
||||
console-grid@2.2.4:
|
||||
resolution: {integrity: sha512-OLjCRTiHhOpTRo9lQp/2FgJDyq5uQHwkEmVJulEnQ6JVf27oKKzXHZnNOv/e72V4++UdMZCrDWtvXW5sx4lyQg==}
|
||||
|
||||
constant-case@3.0.4:
|
||||
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
|
||||
|
||||
|
|
@ -13295,9 +13302,6 @@ packages:
|
|||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
convert-source-map@1.9.0:
|
||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
|
|
@ -13711,10 +13715,6 @@ packages:
|
|||
resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
default-require-extensions@3.0.1:
|
||||
resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
defaults@1.0.4:
|
||||
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
|
||||
|
||||
|
|
@ -14028,6 +14028,9 @@ packages:
|
|||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
eight-colors@1.3.3:
|
||||
resolution: {integrity: sha512-4B54S2Qi4pJjeHmCbDIsveQZWQ/TSSQng4ixYJ9/SYHHpeS5nYK0pzcHvWzWUfRsvJQjwoIENhAwqg59thQceg==}
|
||||
|
||||
ejs@3.1.10:
|
||||
resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -14400,10 +14403,6 @@ packages:
|
|||
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
eslint-visitor-keys@5.0.1:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
eslint@9.29.0:
|
||||
resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
|
@ -14429,10 +14428,6 @@ packages:
|
|||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
espree@11.2.0:
|
||||
resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
esprima-next@5.8.4:
|
||||
resolution: {integrity: sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -14611,6 +14606,10 @@ packages:
|
|||
fake-xml-http-request@2.1.2:
|
||||
resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==}
|
||||
|
||||
fast-check@3.23.2:
|
||||
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
|
|
@ -14745,10 +14744,6 @@ packages:
|
|||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
find-cache-dir@3.3.2:
|
||||
resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up-simple@1.0.1:
|
||||
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -14805,14 +14800,14 @@ packages:
|
|||
foreach@2.0.6:
|
||||
resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
|
||||
|
||||
foreground-child@2.0.0:
|
||||
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
foreground-child@4.0.3:
|
||||
resolution: {integrity: sha512-yeXZaNbCBGaT9giTpLPBdtedzjwhlJBUoL/R4BVQU5mn0TQXOHwVIl1Q2DMuBIdNno4ktA1abZ7dQFVxD6uHxw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
|
||||
|
|
@ -14861,9 +14856,6 @@ packages:
|
|||
from@0.1.7:
|
||||
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
|
||||
|
||||
fromentries@1.3.2:
|
||||
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
|
|
@ -15054,10 +15046,6 @@ packages:
|
|||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@13.0.6:
|
||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
|
||||
|
|
@ -15215,10 +15203,6 @@ packages:
|
|||
hash.js@1.1.7:
|
||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||
|
||||
hasha@5.2.2:
|
||||
resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -15781,10 +15765,6 @@ packages:
|
|||
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-windows@1.0.2:
|
||||
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -15855,10 +15835,6 @@ packages:
|
|||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-hook@3.0.0:
|
||||
resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-instrument@5.2.1:
|
||||
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -15867,10 +15843,6 @@ packages:
|
|||
resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
istanbul-lib-processinfo@2.0.3:
|
||||
resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -16587,9 +16559,6 @@ packages:
|
|||
lodash.flatten@4.4.0:
|
||||
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
||||
|
||||
lodash.flattendeep@4.4.0:
|
||||
resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==}
|
||||
|
||||
lodash.get@4.4.2:
|
||||
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||
|
||||
|
|
@ -16730,6 +16699,9 @@ packages:
|
|||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
lz-utils@2.1.1:
|
||||
resolution: {integrity: sha512-d3Thjos0PSJQAoyMj6vipSSrtrRHS7DImqUNR8x9NW3+zQIftPIbMJAWhi5nPdg5Q9zHz6lxtN8kp/VdMlhi/Q==}
|
||||
|
||||
madge@8.0.0:
|
||||
resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -17399,6 +17371,13 @@ packages:
|
|||
socks:
|
||||
optional: true
|
||||
|
||||
monocart-coverage-reports@2.12.12:
|
||||
resolution: {integrity: sha512-d9FdUr2dn58Crweon0IE0zVi8r/i4vLvfvb2G7eL5vL5LfrxCB2X6F6qzuiwV6RioA4zbAI//7CYi6LjCvN3zA==}
|
||||
hasBin: true
|
||||
|
||||
monocart-locator@1.0.3:
|
||||
resolution: {integrity: sha512-pe29W2XAoA1WQmZZqxXoP7s06ZEXUhcb81086v68cqjk1HnVL7Q/iU/WJnnetxjPcLqwb4qG8vaSGUOMQU602g==}
|
||||
|
||||
mqtt-packet@9.0.0:
|
||||
resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==}
|
||||
|
||||
|
|
@ -17628,10 +17607,6 @@ packages:
|
|||
node-machine-id@1.1.12:
|
||||
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||
|
||||
node-preload@0.2.1:
|
||||
resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
node-readfiles@0.2.0:
|
||||
resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==}
|
||||
|
||||
|
|
@ -17800,11 +17775,6 @@ packages:
|
|||
nwsapi@2.2.7:
|
||||
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
|
||||
|
||||
nyc@17.1.0:
|
||||
resolution: {integrity: sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
oas-kit-common@1.0.8:
|
||||
resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
|
||||
|
||||
|
|
@ -18045,10 +18015,6 @@ packages:
|
|||
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
p-map@3.0.0:
|
||||
resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-map@4.0.0:
|
||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -18093,10 +18059,6 @@ packages:
|
|||
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
package-hash@4.0.0:
|
||||
resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
package-json-from-dist@1.0.0:
|
||||
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
|
||||
|
||||
|
|
@ -18598,10 +18560,6 @@ packages:
|
|||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
process-on-spawn@1.1.0:
|
||||
resolution: {integrity: sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
process@0.11.10:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
|
@ -19033,10 +18991,6 @@ packages:
|
|||
resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
release-zalgo@1.0.0:
|
||||
resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
|
|
@ -19076,9 +19030,6 @@ packages:
|
|||
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
requirejs-config-file@4.0.0:
|
||||
resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
|
@ -19736,10 +19687,6 @@ packages:
|
|||
spawn-command@0.0.2:
|
||||
resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==}
|
||||
|
||||
spawn-wrap@2.0.0:
|
||||
resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
spex@3.3.0:
|
||||
resolution: {integrity: sha512-VNiXjFp6R4ldPbVRYbpxlD35yRHceecVXlct1J4/X80KuuPnW2AXMq3sGwhnJOhKkUsOxAT6nRGfGE5pocVw5w==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
|
@ -20173,10 +20120,6 @@ packages:
|
|||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
test-exclude@8.0.0:
|
||||
resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
testcontainers@11.13.0:
|
||||
resolution: {integrity: sha512-fzTvgOtd6U/esOzgmDatJh79OSK0tU6vjDOJ3B6ICrrJf0dqCWtFdpOr6f/g/KixMxKDTDbszmZYjSORJXsVCQ==}
|
||||
|
||||
|
|
@ -20543,10 +20486,6 @@ packages:
|
|||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.8.1:
|
||||
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
type-fest@2.19.0:
|
||||
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
|
@ -20928,11 +20867,6 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
|
||||
vite-plugin-istanbul@8.0.0:
|
||||
resolution: {integrity: sha512-r6L7cg2iwPqNnY/rWFyemWeDTIKRZjekEWS90e2FsTjDYH4UdTS6hvW1nEX1B++PKPCnqCaj5BJTDn5Cy5jYoQ==}
|
||||
peerDependencies:
|
||||
vite: '>=4'
|
||||
|
||||
vite-plugin-node-polyfills@0.25.0:
|
||||
resolution: {integrity: sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==}
|
||||
peerDependencies:
|
||||
|
|
@ -21304,9 +21238,6 @@ packages:
|
|||
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which-typed-array@1.1.19:
|
||||
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -21379,10 +21310,6 @@ packages:
|
|||
workerpool@6.5.1:
|
||||
resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -21394,9 +21321,6 @@ packages:
|
|||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
write-file-atomic@3.0.3:
|
||||
resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==}
|
||||
|
||||
write-file-atomic@4.0.2:
|
||||
resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
|
||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||
|
|
@ -21521,9 +21445,6 @@ packages:
|
|||
peerDependencies:
|
||||
yjs: ^13.0.0
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
y18n@5.0.8:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -21554,10 +21475,6 @@ packages:
|
|||
resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -21570,10 +21487,6 @@ packages:
|
|||
resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
yargs@16.2.0:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -30815,10 +30728,18 @@ snapshots:
|
|||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn-loose@8.5.2:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
|
||||
acorn@7.4.1: {}
|
||||
|
||||
acorn@8.14.0: {}
|
||||
|
|
@ -31002,10 +30923,6 @@ snapshots:
|
|||
|
||||
append-field@1.0.0: {}
|
||||
|
||||
append-transform@2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions: 3.0.1
|
||||
|
||||
aproba@2.0.0:
|
||||
optional: true
|
||||
|
||||
|
|
@ -31029,8 +30946,6 @@ snapshots:
|
|||
tar-stream: 3.1.7
|
||||
zip-stream: 6.0.1
|
||||
|
||||
archy@1.0.0: {}
|
||||
|
||||
are-we-there-yet@3.0.1:
|
||||
dependencies:
|
||||
delegates: 1.0.0
|
||||
|
|
@ -31722,13 +31637,6 @@ snapshots:
|
|||
hookified: 1.11.0
|
||||
keyv: 5.5.0
|
||||
|
||||
caching-transform@4.0.0:
|
||||
dependencies:
|
||||
hasha: 5.2.2
|
||||
make-dir: 3.1.0
|
||||
package-hash: 4.0.0
|
||||
write-file-atomic: 3.0.3
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -32012,12 +31920,6 @@ snapshots:
|
|||
|
||||
cli-width@4.1.0: {}
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
cliui@7.0.4:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
|
@ -32117,6 +32019,8 @@ snapshots:
|
|||
|
||||
commander@14.0.1: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
|
@ -32218,6 +32122,8 @@ snapshots:
|
|||
console-control-strings@1.1.0:
|
||||
optional: true
|
||||
|
||||
console-grid@2.2.4: {}
|
||||
|
||||
constant-case@3.0.4:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
|
|
@ -32237,8 +32143,6 @@ snapshots:
|
|||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
convert-source-map@1.9.0: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
convict@6.2.5:
|
||||
|
|
@ -32711,10 +32615,6 @@ snapshots:
|
|||
bundle-name: 4.1.0
|
||||
default-browser-id: 5.0.1
|
||||
|
||||
default-require-extensions@3.0.1:
|
||||
dependencies:
|
||||
strip-bom: 4.0.0
|
||||
|
||||
defaults@1.0.4:
|
||||
dependencies:
|
||||
clone: 1.0.4
|
||||
|
|
@ -33065,6 +32965,8 @@ snapshots:
|
|||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
eight-colors@1.3.3: {}
|
||||
|
||||
ejs@3.1.10:
|
||||
dependencies:
|
||||
jake: 10.8.5
|
||||
|
|
@ -33355,7 +33257,8 @@ snapshots:
|
|||
esniff: 2.0.1
|
||||
next-tick: 1.1.0
|
||||
|
||||
es6-error@4.1.1: {}
|
||||
es6-error@4.1.1:
|
||||
optional: true
|
||||
|
||||
es6-iterator@2.0.3:
|
||||
dependencies:
|
||||
|
|
@ -33625,8 +33528,6 @@ snapshots:
|
|||
|
||||
eslint-visitor-keys@4.2.1: {}
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@9.29.0(jiti@2.6.1):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.6.1))
|
||||
|
|
@ -33693,12 +33594,6 @@ snapshots:
|
|||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||
eslint-visitor-keys: 4.2.1
|
||||
|
||||
espree@11.2.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn-jsx: 5.3.2(acorn@8.16.0)
|
||||
eslint-visitor-keys: 5.0.1
|
||||
|
||||
esprima-next@5.8.4: {}
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
|
@ -33964,6 +33859,10 @@ snapshots:
|
|||
|
||||
fake-xml-http-request@2.1.2: {}
|
||||
|
||||
fast-check@3.23.2:
|
||||
dependencies:
|
||||
pure-rand: 6.1.0
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
|
@ -34118,12 +34017,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
find-cache-dir@3.3.2:
|
||||
dependencies:
|
||||
commondir: 1.0.1
|
||||
make-dir: 3.1.0
|
||||
pkg-dir: 4.2.0
|
||||
|
||||
find-up-simple@1.0.1: {}
|
||||
|
||||
find-up@3.0.0:
|
||||
|
|
@ -34174,16 +34067,15 @@ snapshots:
|
|||
|
||||
foreach@2.0.6: {}
|
||||
|
||||
foreground-child@2.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 3.0.7
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
foreground-child@4.0.3:
|
||||
dependencies:
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
|
||||
form-data-encoder@4.0.2: {}
|
||||
|
|
@ -34225,8 +34117,6 @@ snapshots:
|
|||
|
||||
from@0.1.7: {}
|
||||
|
||||
fromentries@1.3.2: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fs-extra@10.1.0:
|
||||
|
|
@ -34472,12 +34362,6 @@ snapshots:
|
|||
package-json-from-dist: 1.0.0
|
||||
path-scurry: 2.0.2
|
||||
|
||||
glob@13.0.6:
|
||||
dependencies:
|
||||
minimatch: 10.2.3
|
||||
minipass: 7.1.3
|
||||
path-scurry: 2.0.2
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
|
|
@ -34708,11 +34592,6 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
hasha@5.2.2:
|
||||
dependencies:
|
||||
is-stream: 2.0.1
|
||||
type-fest: 0.8.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
|
@ -35325,8 +35204,6 @@ snapshots:
|
|||
call-bound: 1.0.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
is-windows@1.0.2: {}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
|
|
@ -35373,10 +35250,6 @@ snapshots:
|
|||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-hook@3.0.0:
|
||||
dependencies:
|
||||
append-transform: 2.0.0
|
||||
|
||||
istanbul-lib-instrument@5.2.1:
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
|
@ -35397,15 +35270,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
istanbul-lib-processinfo@2.0.3:
|
||||
dependencies:
|
||||
archy: 1.0.0
|
||||
cross-spawn: 7.0.6
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
p-map: 3.0.0
|
||||
rimraf: 3.0.2
|
||||
uuid: 13.0.1
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
dependencies:
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
|
|
@ -36445,8 +36309,6 @@ snapshots:
|
|||
|
||||
lodash.flatten@4.4.0: {}
|
||||
|
||||
lodash.flattendeep@4.4.0: {}
|
||||
|
||||
lodash.get@4.4.2: {}
|
||||
|
||||
lodash.groupby@4.6.0: {}
|
||||
|
|
@ -36556,6 +36418,8 @@ snapshots:
|
|||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
lz-utils@2.1.1: {}
|
||||
|
||||
madge@8.0.0(typescript@6.0.2):
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
|
|
@ -37579,6 +37443,23 @@ snapshots:
|
|||
gcp-metadata: 5.3.0
|
||||
socks: 2.8.3
|
||||
|
||||
monocart-coverage-reports@2.12.12:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
acorn-loose: 8.5.2
|
||||
acorn-walk: 8.3.5
|
||||
commander: 14.0.3
|
||||
console-grid: 2.2.4
|
||||
eight-colors: 1.3.3
|
||||
foreground-child: 4.0.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-reports: 3.2.0
|
||||
lz-utils: 2.1.1
|
||||
monocart-locator: 1.0.3
|
||||
|
||||
monocart-locator@1.0.3: {}
|
||||
|
||||
mqtt-packet@9.0.0:
|
||||
dependencies:
|
||||
bl: 6.0.12
|
||||
|
|
@ -37844,10 +37725,6 @@ snapshots:
|
|||
|
||||
node-machine-id@1.1.12: {}
|
||||
|
||||
node-preload@0.2.1:
|
||||
dependencies:
|
||||
process-on-spawn: 1.1.0
|
||||
|
||||
node-readfiles@0.2.0:
|
||||
dependencies:
|
||||
es6-promise: 3.3.1
|
||||
|
|
@ -38042,38 +37919,6 @@ snapshots:
|
|||
|
||||
nwsapi@2.2.7: {}
|
||||
|
||||
nyc@17.1.0:
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
caching-transform: 4.0.0
|
||||
convert-source-map: 1.9.0
|
||||
decamelize: 1.2.0
|
||||
find-cache-dir: 3.3.2
|
||||
find-up: 4.1.0
|
||||
foreground-child: 3.3.1
|
||||
get-package-type: 0.1.0
|
||||
glob: 7.2.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-hook: 3.0.0
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
istanbul-lib-processinfo: 2.0.3
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 4.0.1
|
||||
istanbul-reports: 3.2.0
|
||||
make-dir: 3.1.0
|
||||
node-preload: 0.2.1
|
||||
p-map: 3.0.0
|
||||
process-on-spawn: 1.1.0
|
||||
resolve-from: 5.0.0
|
||||
rimraf: 3.0.2
|
||||
signal-exit: 3.0.7
|
||||
spawn-wrap: 2.0.0
|
||||
test-exclude: 6.0.0
|
||||
yargs: 15.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
oas-kit-common@1.0.8:
|
||||
dependencies:
|
||||
fast-safe-stringify: 2.1.1
|
||||
|
|
@ -38350,10 +38195,6 @@ snapshots:
|
|||
dependencies:
|
||||
p-limit: 4.0.0
|
||||
|
||||
p-map@3.0.0:
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
|
||||
p-map@4.0.0:
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
|
|
@ -38405,13 +38246,6 @@ snapshots:
|
|||
degenerator: 5.0.1
|
||||
netmask: 2.1.1
|
||||
|
||||
package-hash@4.0.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
hasha: 5.2.2
|
||||
lodash.flattendeep: 4.4.0
|
||||
release-zalgo: 1.0.0
|
||||
|
||||
package-json-from-dist@1.0.0: {}
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
|
@ -38891,10 +38725,6 @@ snapshots:
|
|||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process-on-spawn@1.1.0:
|
||||
dependencies:
|
||||
fromentries: 1.3.2
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
|
|
@ -39500,10 +39330,6 @@ snapshots:
|
|||
|
||||
relateurl@0.2.7: {}
|
||||
|
||||
release-zalgo@1.0.0:
|
||||
dependencies:
|
||||
es6-error: 4.1.1
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
|
@ -39565,8 +39391,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
requirejs-config-file@4.0.0:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
|
|
@ -39653,6 +39477,7 @@ snapshots:
|
|||
rimraf@3.0.2:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
optional: true
|
||||
|
||||
rimraf@5.0.1:
|
||||
dependencies:
|
||||
|
|
@ -40086,7 +39911,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
set-blocking@2.0.0:
|
||||
optional: true
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
|
|
@ -40382,15 +40208,6 @@ snapshots:
|
|||
|
||||
spawn-command@0.0.2: {}
|
||||
|
||||
spawn-wrap@2.0.0:
|
||||
dependencies:
|
||||
foreground-child: 2.0.0
|
||||
is-windows: 1.0.2
|
||||
make-dir: 3.1.0
|
||||
rimraf: 3.0.2
|
||||
signal-exit: 3.0.7
|
||||
which: 2.0.2
|
||||
|
||||
spex@3.3.0: {}
|
||||
|
||||
split-ca@1.0.1: {}
|
||||
|
|
@ -40991,12 +40808,6 @@ snapshots:
|
|||
glob: 7.2.3
|
||||
minimatch: 5.1.8
|
||||
|
||||
test-exclude@8.0.0:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 13.0.6
|
||||
minimatch: 10.2.3
|
||||
|
||||
testcontainers@11.13.0:
|
||||
dependencies:
|
||||
'@balena/dockerignore': 1.0.2
|
||||
|
|
@ -41371,8 +41182,6 @@ snapshots:
|
|||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
type-fest@0.8.1: {}
|
||||
|
||||
type-fest@2.19.0: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
|
@ -41785,20 +41594,6 @@ snapshots:
|
|||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-istanbul@8.0.0(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.1
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
'@types/babel__generator': 7.27.0
|
||||
espree: 11.2.0
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
picocolors: 1.1.1
|
||||
source-map: 0.7.6
|
||||
test-exclude: 8.0.0
|
||||
vite: 8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-node-polyfills@0.25.0(rollup@4.52.4)(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/plugin-inject': 5.0.5(rollup@4.52.4)
|
||||
|
|
@ -42222,8 +42017,6 @@ snapshots:
|
|||
is-weakmap: 2.0.2
|
||||
is-weakset: 2.0.4
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which-typed-array@1.1.19:
|
||||
dependencies:
|
||||
available-typed-arrays: 1.0.7
|
||||
|
|
@ -42330,12 +42123,6 @@ snapshots:
|
|||
|
||||
workerpool@6.5.1: {}
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
|
|
@ -42350,13 +42137,6 @@ snapshots:
|
|||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
write-file-atomic@3.0.3:
|
||||
dependencies:
|
||||
imurmurhash: 0.1.4
|
||||
is-typedarray: 1.0.0
|
||||
signal-exit: 3.0.7
|
||||
typedarray-to-buffer: 3.1.5
|
||||
|
||||
write-file-atomic@4.0.2:
|
||||
dependencies:
|
||||
imurmurhash: 0.1.4
|
||||
|
|
@ -42443,8 +42223,6 @@ snapshots:
|
|||
lib0: 0.2.117
|
||||
yjs: 13.6.29
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yaeti@0.0.6: {}
|
||||
|
|
@ -42464,11 +42242,6 @@ snapshots:
|
|||
argparse: 1.0.10
|
||||
glob: 7.2.3
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs-parser@20.2.9: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
|
@ -42480,20 +42253,6 @@ snapshots:
|
|||
flat: 5.0.2
|
||||
is-plain-obj: 2.1.0
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
yargs@16.2.0:
|
||||
dependencies:
|
||||
cliui: 7.0.4
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ catalog:
|
|||
csv-parse: 6.2.1
|
||||
eslint: 9.29.0
|
||||
eslint-plugin-oxlint: ^1.61.0
|
||||
fast-check: ^3.23.2
|
||||
fast-glob: 3.2.12
|
||||
fast-json-patch: ^3.1.1
|
||||
fastest-levenshtein: 1.0.16
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user