From d0ea26513311a031a38fca90f4ab5b87402e0776 Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 11:21:06 +0100 Subject: [PATCH 1/6] fix: add tsx as devDependency to prevent E2E test race condition When 16 E2E tests run in parallel, each invokes 'npx tsx' to start the test server. Without tsx installed locally, all 16 processes race to download and install tsx into ~/.npm/_npx/ simultaneously, causing ENOTEMPTY, TAR_ENTRY_ERROR, and 'tsx: not found' failures. Adding tsx as a devDependency ensures it's available locally and npx resolves it instantly without any network/install race. --- package-lock.json | 540 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 541 insertions(+) diff --git a/package-lock.json b/package-lock.json index dfe1e35..7fa0a2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "nyc": "^18.0.0", "prettier": "^3.7.4", "ts-jest": "^29.4.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" }, "engines": { @@ -75,6 +76,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -597,6 +599,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2227,6 +2671,7 @@ "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2325,6 +2770,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2818,6 +3264,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3402,6 +3849,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4831,6 +5279,48 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4889,6 +5379,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5239,6 +5730,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5789,6 +6281,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -6076,6 +6581,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz", "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -7105,6 +7611,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10439,6 +10946,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -11511,6 +12028,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11662,6 +12180,26 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11829,6 +12367,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12550,6 +13089,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 25eb5ea..ae79f48 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "nyc": "^18.0.0", "prettier": "^3.7.4", "ts-jest": "^29.4.6", + "tsx": "^4.21.0", "typescript": "^5.9.3" } } From 3e1fc6698769df801c86c9c88302c07ff2cac72d Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 11:35:37 +0100 Subject: [PATCH 2/6] fix: stabilize flaky E2E tests with isolation and readiness polling - bridge-resilience: use --isolated to prevent shared sessions.json pollution from parallel tests causing INVARIANT VIOLATION - header-security: replace hardcoded sleep with wait_for ping polling to ensure bridge is ready before assertions - proxy: replace all hardcoded sleeps with wait_for health endpoint polling; use --isolated to avoid session file contention - server-abort: add wait_for health check after server reset and ping polling after connect; use --isolated --- test/e2e/suites/basic/header-security.test.sh | 7 +++++-- test/e2e/suites/sessions/bridge-resilience.test.sh | 2 +- test/e2e/suites/sessions/proxy.test.sh | 14 +++++++++----- test/e2e/suites/sessions/server-abort.test.sh | 7 ++++++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/test/e2e/suites/basic/header-security.test.sh b/test/e2e/suites/basic/header-security.test.sh index 01d95d0..c48e40b 100755 --- a/test/e2e/suites/basic/header-security.test.sh +++ b/test/e2e/suites/basic/header-security.test.sh @@ -26,8 +26,8 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION" --header "$SECRET_HEADER: Bearer assert_success _SESSIONS_CREATED+=("$SESSION") -# Give the bridge process time to start -sleep 1 +# Wait for bridge to be fully ready +wait_for "$MCPC $SESSION ping >/dev/null 2>&1" # Check that the secret is not visible in process list # This tests that we're not passing credentials as command-line arguments @@ -128,6 +128,9 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" \ assert_success _SESSIONS_CREATED+=("$SESSION2") +# Wait for bridge to be fully ready +wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" + # None of the secrets should appear in sessions.json sessions_content=$(cat "$sessions_file") if echo "$sessions_content" | grep -q "$SECRET_VALUE"; then diff --git a/test/e2e/suites/sessions/bridge-resilience.test.sh b/test/e2e/suites/sessions/bridge-resilience.test.sh index 64d74ed..3ae97fb 100755 --- a/test/e2e/suites/sessions/bridge-resilience.test.sh +++ b/test/e2e/suites/sessions/bridge-resilience.test.sh @@ -2,7 +2,7 @@ # Test: Bridge resilience to MCP errors source "$(dirname "$0")/../../lib/framework.sh" -test_init "sessions/bridge-resilience" +test_init "sessions/bridge-resilience" --isolated # Start test server start_test_server diff --git a/test/e2e/suites/sessions/proxy.test.sh b/test/e2e/suites/sessions/proxy.test.sh index 0ffae99..679ade1 100755 --- a/test/e2e/suites/sessions/proxy.test.sh +++ b/test/e2e/suites/sessions/proxy.test.sh @@ -4,7 +4,7 @@ # that forwards requests without exposing original auth tokens source "$(dirname "$0")/../../lib/framework.sh" -test_init "sessions/proxy" +test_init "sessions/proxy" --isolated # Start test server start_test_server @@ -24,8 +24,8 @@ assert_contains "$STDOUT" "created" _SESSIONS_CREATED+=("$SESSION_UPSTREAM") test_pass -# Wait for proxy server to start -sleep 1 +# Wait for proxy server to be ready +wait_for "curl -s http://127.0.0.1:$PROXY_PORT/health 2>/dev/null | grep -q ok" # Test: session shows proxy info test_case "session shows proxy info in list" @@ -51,6 +51,8 @@ test_pass # Test: can connect to proxy as MCP server (localhost defaults to http://) test_case "connect to proxy server" +# Ensure proxy is still healthy before connecting downstream +wait_for "curl -s http://127.0.0.1:$PROXY_PORT/health 2>/dev/null | grep -q ok" run_mcpc "127.0.0.1:$PROXY_PORT" connect "$SESSION_DOWNSTREAM" assert_success "connect to proxy should succeed" _SESSIONS_CREATED+=("$SESSION_DOWNSTREAM") @@ -114,7 +116,8 @@ assert_success _SESSIONS_CREATED+=("$SESSION_CONFLICT1") test_pass -sleep 1 +# Wait for proxy to be ready before testing conflict +wait_for "curl -s http://127.0.0.1:$PROXY_PORT_CONFLICT/health 2>/dev/null | grep -q ok" # Test: second session on same port should fail test_case "second session on same port fails" @@ -145,7 +148,8 @@ assert_success "connect with --proxy-bearer-token should succeed" _SESSIONS_CREATED+=("$SESSION_AUTH") test_pass -sleep 1 +# Wait for proxy to be ready +wait_for "curl -s http://127.0.0.1:$PROXY_PORT_AUTH/health 2>/dev/null | grep -q ok" # Test: health endpoint works without auth (health is public) test_case "proxy health endpoint works without auth" diff --git a/test/e2e/suites/sessions/server-abort.test.sh b/test/e2e/suites/sessions/server-abort.test.sh index c23c409..14d3b0a 100755 --- a/test/e2e/suites/sessions/server-abort.test.sh +++ b/test/e2e/suites/sessions/server-abort.test.sh @@ -2,7 +2,7 @@ # Test: Server-side session abort handling source "$(dirname "$0")/../../lib/framework.sh" -test_init "sessions/server-abort" +test_init "sessions/server-abort" --isolated # Start test server start_test_server @@ -43,6 +43,8 @@ test_pass # Test: reset server state test_case "reset server state" curl -s -X POST "$TEST_SERVER_URL/control/reset" >/dev/null +# Wait for server to be healthy after reset +wait_for "curl -s $TEST_SERVER_URL/health 2>/dev/null | grep -q ok" test_pass # Test: session can be recreated after server reset @@ -57,6 +59,9 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" assert_success _SESSIONS_CREATED+=("$SESSION2") +# Wait for bridge to be fully ready before running invariant checks +wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" + run_xmcpc "$SESSION2" tools-list assert_success test_pass From 6ab9ec8415c401d656b2386465256e580c9763d7 Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 11:48:04 +0100 Subject: [PATCH 3/6] fix: don't fail connect when post-creation health check races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connect command was failing with exit code 1 when showServerDetails() couldn't reach the bridge fast enough after session creation. The session was already created successfully — the bridge just needed more time to complete MCP initialization. Now showServerDetails is best-effort on connect: if it fails, the session is still valid and the next command will trigger ensureBridgeReady which handles retries properly. --- src/cli/commands/sessions.ts | 18 +++++++++++++----- test/e2e/suites/basic/header-security.test.sh | 3 --- test/e2e/suites/sessions/server-abort.test.sh | 3 --- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/cli/commands/sessions.ts b/src/cli/commands/sessions.ts index 346bd37..a70bf58 100644 --- a/src/cli/commands/sessions.ts +++ b/src/cli/commands/sessions.ts @@ -261,11 +261,19 @@ export async function connectSession( console.log(formatSuccess(`Session ${name} ${isReconnect ? 'reconnected' : 'created'}`)); } - // Display server info via the new session - await showServerDetails(name, { - ...options, - hideTarget: false, // Show session info prefix - }); + // Display server info via the new session (best-effort) + // If bridge is still initializing, don't fail the connect — the session is already created. + // The next command (e.g. ping, tools-list) will wait for the bridge to be ready. + try { + await showServerDetails(name, { + ...options, + hideTarget: false, // Show session info prefix + }); + } catch (detailsError) { + logger.debug( + `showServerDetails failed for new session ${name}: ${(detailsError as Error).message}` + ); + } } catch (error) { if (options.outputMode === 'human') { console.error(formatError((error as Error).message)); diff --git a/test/e2e/suites/basic/header-security.test.sh b/test/e2e/suites/basic/header-security.test.sh index c48e40b..4df4c17 100755 --- a/test/e2e/suites/basic/header-security.test.sh +++ b/test/e2e/suites/basic/header-security.test.sh @@ -128,9 +128,6 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" \ assert_success _SESSIONS_CREATED+=("$SESSION2") -# Wait for bridge to be fully ready -wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" - # None of the secrets should appear in sessions.json sessions_content=$(cat "$sessions_file") if echo "$sessions_content" | grep -q "$SECRET_VALUE"; then diff --git a/test/e2e/suites/sessions/server-abort.test.sh b/test/e2e/suites/sessions/server-abort.test.sh index 14d3b0a..aad037d 100755 --- a/test/e2e/suites/sessions/server-abort.test.sh +++ b/test/e2e/suites/sessions/server-abort.test.sh @@ -59,9 +59,6 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" assert_success _SESSIONS_CREATED+=("$SESSION2") -# Wait for bridge to be fully ready before running invariant checks -wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" - run_xmcpc "$SESSION2" tools-list assert_success test_pass From 4b9fa883aabc9b93db664fcc913b6deed776b145 Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 11:54:45 +0100 Subject: [PATCH 4/6] fix: re-throw auth errors from showServerDetails in connect The previous commit swallowed all errors from showServerDetails, including auth errors. This caused 'mcpc connect @name' to succeed even when the server requires authentication. Auth errors must propagate so users get the login hint. --- src/cli/commands/sessions.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/sessions.ts b/src/cli/commands/sessions.ts index a70bf58..51fd0e0 100644 --- a/src/cli/commands/sessions.ts +++ b/src/cli/commands/sessions.ts @@ -34,7 +34,7 @@ import { storeKeychainSessionHeaders, storeKeychainProxyBearerToken, } from '../../lib/auth/keychain.js'; -import { ClientError } from '../../lib/index.js'; +import { AuthError, ClientError } from '../../lib/index.js'; import chalk from 'chalk'; import { createLogger } from '../../lib/logger.js'; import { parseProxyArg } from '../parser.js'; @@ -270,6 +270,10 @@ export async function connectSession( hideTarget: false, // Show session info prefix }); } catch (detailsError) { + // Re-throw auth errors — these are real failures, not timing issues + if (detailsError instanceof AuthError) { + throw detailsError; + } logger.debug( `showServerDetails failed for new session ${name}: ${(detailsError as Error).message}` ); From ae1a395d411ed70ab2fa9978cb2678931b3c78ff Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 17:11:35 +0100 Subject: [PATCH 5/6] fix: retry bridge health check before restarting on transient socket errors ensureBridgeReady() now retries up to 3 times (1s apart) when the bridge socket is temporarily unresponsive, instead of immediately restarting the bridge. This prevents false-positive restarts caused by transient event loop stalls (common under CI load). Also adds wait_for ping readiness polling after connect in 3 E2E tests (header-security, proxy, server-abort) as defense-in-depth. --- src/lib/bridge-manager.ts | 51 ++++++++++++------- test/e2e/suites/basic/header-security.test.sh | 3 ++ test/e2e/suites/sessions/proxy.test.sh | 3 ++ test/e2e/suites/sessions/server-abort.test.sh | 3 ++ 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/lib/bridge-manager.ts b/src/lib/bridge-manager.ts index 2cd98ee..fb6ad3f 100644 --- a/src/lib/bridge-manager.ts +++ b/src/lib/bridge-manager.ts @@ -432,25 +432,40 @@ export async function ensureBridgeReady(sessionName: string): Promise { if (processAlive) { // Process alive, try getServerDetails (blocks until MCP connected) - const result = await checkBridgeHealth(socketPath); - if (result.healthy) { - logger.debug(`Bridge for ${sessionName} is healthy`); - return socketPath; - } - // Not healthy - check if it's a connection issue vs MCP error - if (result.error instanceof NetworkError) { - logger.warn(`Bridge process alive but socket not responding for ${sessionName}`); - } else if (result.error) { - // MCP connection error - check if it's an auth error - const errorMessage = result.error.message || ''; - if (isAuthenticationError(errorMessage)) { - const target = session.server.url || session.server.command || sessionName; - throw createServerAuthError(target, { sessionName, originalError: result.error }); + // Retry on transient socket errors (e.g. bridge event loop busy during MCP init) + const MAX_HEALTH_RETRIES = 3; + const HEALTH_RETRY_DELAY_MS = 1000; + + for (let attempt = 1; attempt <= MAX_HEALTH_RETRIES; attempt++) { + const result = await checkBridgeHealth(socketPath); + if (result.healthy) { + logger.debug(`Bridge for ${sessionName} is healthy`); + return socketPath; } - // Other MCP errors - propagate - throw new ClientError( - `Bridge for ${sessionName} failed to connect to MCP server: ${result.error.message}` - ); + // Not healthy - check if it's a connection issue vs MCP error + if (result.error instanceof NetworkError) { + // Socket not responding - could be transient (bridge busy with MCP init) + if (attempt < MAX_HEALTH_RETRIES && isProcessAlive(session.pid!)) { + logger.warn( + `Bridge socket not responding for ${sessionName} (attempt ${attempt}/${MAX_HEALTH_RETRIES}), retrying...` + ); + await new Promise((resolve) => setTimeout(resolve, HEALTH_RETRY_DELAY_MS)); + continue; + } + logger.warn(`Bridge process alive but socket not responding for ${sessionName}`); + } else if (result.error) { + // MCP connection error - check if it's an auth error + const errorMessage = result.error.message || ''; + if (isAuthenticationError(errorMessage)) { + const target = session.server.url || session.server.command || sessionName; + throw createServerAuthError(target, { sessionName, originalError: result.error }); + } + // Other MCP errors - propagate + throw new ClientError( + `Bridge for ${sessionName} failed to connect to MCP server: ${result.error.message}` + ); + } + break; // Non-retryable error, fall through to restart } } else { logger.debug(`Bridge process not alive for ${sessionName}, will try to restart it`); diff --git a/test/e2e/suites/basic/header-security.test.sh b/test/e2e/suites/basic/header-security.test.sh index 4df4c17..0b8877e 100755 --- a/test/e2e/suites/basic/header-security.test.sh +++ b/test/e2e/suites/basic/header-security.test.sh @@ -128,6 +128,9 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" \ assert_success _SESSIONS_CREATED+=("$SESSION2") +# Wait for bridge to be fully ready before using session +wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" + # None of the secrets should appear in sessions.json sessions_content=$(cat "$sessions_file") if echo "$sessions_content" | grep -q "$SECRET_VALUE"; then diff --git a/test/e2e/suites/sessions/proxy.test.sh b/test/e2e/suites/sessions/proxy.test.sh index 679ade1..0c3716d 100755 --- a/test/e2e/suites/sessions/proxy.test.sh +++ b/test/e2e/suites/sessions/proxy.test.sh @@ -56,6 +56,9 @@ wait_for "curl -s http://127.0.0.1:$PROXY_PORT/health 2>/dev/null | grep -q ok" run_mcpc "127.0.0.1:$PROXY_PORT" connect "$SESSION_DOWNSTREAM" assert_success "connect to proxy should succeed" _SESSIONS_CREATED+=("$SESSION_DOWNSTREAM") + +# Wait for downstream bridge to be fully ready +wait_for "$MCPC $SESSION_DOWNSTREAM ping >/dev/null 2>&1" test_pass # Test: proxy passes through upstream server instructions diff --git a/test/e2e/suites/sessions/server-abort.test.sh b/test/e2e/suites/sessions/server-abort.test.sh index aad037d..6c8199d 100755 --- a/test/e2e/suites/sessions/server-abort.test.sh +++ b/test/e2e/suites/sessions/server-abort.test.sh @@ -59,6 +59,9 @@ run_mcpc "$TEST_SERVER_URL" connect "$SESSION2" assert_success _SESSIONS_CREATED+=("$SESSION2") +# Wait for bridge to be fully ready after reconnection +wait_for "$MCPC $SESSION2 ping >/dev/null 2>&1" + run_xmcpc "$SESSION2" tools-list assert_success test_pass From 229b8a03615bddb883ed19e0bd9b33749327644b Mon Sep 17 00:00:00 2001 From: MQ37 Date: Sat, 28 Feb 2026 17:23:06 +0100 Subject: [PATCH 6/6] revert: remove bridge health check retry that worsened lock contention The retry logic in ensureBridgeReady() added up to 12s of extra latency per failed health check, causing cascading lock contention on the shared sessions.json across 16 parallel E2E tests. Revert to single-shot check; the wait_for ping additions in the test scripts are sufficient. --- src/lib/bridge-manager.ts | 51 ++++++++++++++------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/lib/bridge-manager.ts b/src/lib/bridge-manager.ts index fb6ad3f..2cd98ee 100644 --- a/src/lib/bridge-manager.ts +++ b/src/lib/bridge-manager.ts @@ -432,40 +432,25 @@ export async function ensureBridgeReady(sessionName: string): Promise { if (processAlive) { // Process alive, try getServerDetails (blocks until MCP connected) - // Retry on transient socket errors (e.g. bridge event loop busy during MCP init) - const MAX_HEALTH_RETRIES = 3; - const HEALTH_RETRY_DELAY_MS = 1000; - - for (let attempt = 1; attempt <= MAX_HEALTH_RETRIES; attempt++) { - const result = await checkBridgeHealth(socketPath); - if (result.healthy) { - logger.debug(`Bridge for ${sessionName} is healthy`); - return socketPath; - } - // Not healthy - check if it's a connection issue vs MCP error - if (result.error instanceof NetworkError) { - // Socket not responding - could be transient (bridge busy with MCP init) - if (attempt < MAX_HEALTH_RETRIES && isProcessAlive(session.pid!)) { - logger.warn( - `Bridge socket not responding for ${sessionName} (attempt ${attempt}/${MAX_HEALTH_RETRIES}), retrying...` - ); - await new Promise((resolve) => setTimeout(resolve, HEALTH_RETRY_DELAY_MS)); - continue; - } - logger.warn(`Bridge process alive but socket not responding for ${sessionName}`); - } else if (result.error) { - // MCP connection error - check if it's an auth error - const errorMessage = result.error.message || ''; - if (isAuthenticationError(errorMessage)) { - const target = session.server.url || session.server.command || sessionName; - throw createServerAuthError(target, { sessionName, originalError: result.error }); - } - // Other MCP errors - propagate - throw new ClientError( - `Bridge for ${sessionName} failed to connect to MCP server: ${result.error.message}` - ); + const result = await checkBridgeHealth(socketPath); + if (result.healthy) { + logger.debug(`Bridge for ${sessionName} is healthy`); + return socketPath; + } + // Not healthy - check if it's a connection issue vs MCP error + if (result.error instanceof NetworkError) { + logger.warn(`Bridge process alive but socket not responding for ${sessionName}`); + } else if (result.error) { + // MCP connection error - check if it's an auth error + const errorMessage = result.error.message || ''; + if (isAuthenticationError(errorMessage)) { + const target = session.server.url || session.server.command || sessionName; + throw createServerAuthError(target, { sessionName, originalError: result.error }); } - break; // Non-retryable error, fall through to restart + // Other MCP errors - propagate + throw new ClientError( + `Bridge for ${sessionName} failed to connect to MCP server: ${result.error.message}` + ); } } else { logger.debug(`Bridge process not alive for ${sessionName}, will try to restart it`);