From f3a1c577a74bb83b955c5227e15f2b3b73ee15aa Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:22:11 +0200 Subject: [PATCH 1/9] add e2e signalling tests --- .github/workflows/e2e.yaml | 64 +++++++ package.json | 11 +- pnpm-lock.yaml | 223 +++++++++++++++++++----- src/api/SignalClient.e2e.test.ts | 282 +++++++++++++++++++++++++++++++ src/test/signalServerSetup.ts | 147 ++++++++++++++++ src/test/signalToken.ts | 59 +++++++ src/test/vitest-context.d.ts | 10 ++ tsconfig.json | 2 +- vite.config.mjs | 3 + vitest.e2e.config.mts | 24 +++ 10 files changed, 776 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/e2e.yaml create mode 100644 src/api/SignalClient.e2e.test.ts create mode 100644 src/test/signalServerSetup.ts create mode 100644 src/test/signalToken.ts create mode 100644 src/test/vitest-context.d.ts create mode 100644 vitest.e2e.config.mts diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 0000000000..0c642080ad --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,64 @@ +name: E2E + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + e2e: + name: Signal e2e (browser / ubuntu) + runs-on: ubuntu-latest + + services: + testserver: + image: livekit/test-server:latest + ports: + - 9999:9999 + + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + + - name: Use Node.js 24 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Cache Playwright browsers + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} + + - name: Install Playwright Chromium + run: pnpm exec playwright install --with-deps chromium + + - name: Run signal e2e tests + run: pnpm test:e2e 2>&1 | tee e2e.log + env: + # Connect to the test-server service above (validate + WS on 9999). + LK_TEST_SERVER_URL: ws://127.0.0.1:9999 + + - name: Collect test-server logs + if: failure() + run: docker logs ${{ job.services.testserver.id }} > test-server.log 2>&1 || true + + - name: Upload logs + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: e2e-logs + path: | + e2e.log + test-server.log + retention-days: 7 diff --git a/package.json b/package.json index dda202ea07..c36aa62397 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,11 @@ "build-docs": "typedoc && mkdir -p docs/assets/github && cp .github/*.png docs/assets/github/ && find docs -name '*.html' -type f -exec sed -i.bak 's|=\"/.github/|=\"assets/github/|g' {} + && find docs -name '*.bak' -delete", "proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto", "examples:demo": "vite examples/demo -c vite.config.mjs", + "examples:signal-fsm": "vite examples/signal-connection-fsm -c vite.config.mjs", "dev": "pnpm examples:demo", "lint": "eslint src", "test": "vitest run src", + "test:e2e": "vitest run --config vitest.e2e.config.mts", "deploy": "gh-pages -d examples/demo/dist", "format": "prettier --write src examples/**/*.ts", "format:check": "prettier --check src examples/**/*.ts", @@ -78,7 +80,9 @@ "sdp-transform": "^2.15.0", "tslib": "2.8.1", "typed-emitter": "^2.1.0", - "webrtc-adapter": "9.0.6" + "typescript-fsm": "^1.6.0", + "webrtc-adapter": "9.0.6", + "xstate": "^5.32.4" }, "peerDependencies": { "@types/dom-mediacapture-record": "^1" @@ -105,6 +109,8 @@ "@types/ua-parser-js": "0.7.39", "@typescript-eslint/eslint-plugin": "8.59.4", "@typescript-eslint/parser": "8.59.4", + "@vitest/browser": "^4.1.10", + "@vitest/browser-playwright": "^4.1.10", "downlevel-dts": "^0.11.0", "eslint": "10.4.0", "eslint-config-airbnb-extended": "^2.3.2", @@ -116,6 +122,7 @@ "glob": "^13.0.6", "happy-dom": "^20.0.0", "jsdom": "^26.1.0", + "playwright": "^1.61.1", "prettier": "^3.4.2", "publint": "^0.3.21", "rollup": "4.60.4", @@ -128,7 +135,7 @@ "typescript": "5.9.3", "typescript-eslint": "^8.47.0", "vite": "7.3.5", - "vitest": "^4.0.0" + "vitest": "^4.1.10" }, "packageManager": "pnpm@10.33.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e02bbc55ac..c958894e1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,15 @@ importers: typed-emitter: specifier: ^2.1.0 version: 2.1.0 + typescript-fsm: + specifier: ^1.6.0 + version: 1.6.0 webrtc-adapter: specifier: 9.0.6 version: 9.0.6 + xstate: + specifier: ^5.32.4 + version: 5.32.4 devDependencies: '@babel/core': specifier: 7.29.7 @@ -102,6 +108,12 @@ importers: '@typescript-eslint/parser': specifier: 8.59.4 version: 8.59.4(eslint@10.4.0(jiti@2.4.2))(typescript@5.9.3) + '@vitest/browser': + specifier: ^4.1.10 + version: 4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10) + '@vitest/browser-playwright': + specifier: ^4.1.10 + version: 4.1.10(playwright@1.61.1)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10) downlevel-dts: specifier: ^0.11.0 version: 0.11.0 @@ -135,6 +147,9 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 + playwright: + specifier: ^1.61.1 + version: 1.61.1 prettier: specifier: ^3.4.2 version: 3.8.3 @@ -172,8 +187,8 @@ importers: specifier: 7.3.5 version: 7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4) vitest: - specifier: ^4.0.0 - version: 4.1.7(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + specifier: ^4.1.10 + version: 4.1.10(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) packages: @@ -685,6 +700,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@bufbuild/protobuf@1.10.1': resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==} @@ -1249,6 +1267,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@publint/pack@0.1.4': resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} @@ -1752,11 +1773,22 @@ packages: cpu: [x64] os: [win32] - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/browser-playwright@4.1.10': + resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} + peerDependencies: + playwright: '*' + vitest: 4.1.10 + + '@vitest/browser@4.1.10': + resolution: {integrity: sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==} + peerDependencies: + vitest: 4.1.10 + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1766,20 +1798,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2539,6 +2571,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3023,6 +3060,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3198,6 +3239,20 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3427,6 +3482,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + size-limit@11.2.0: resolution: {integrity: sha512-2kpQq2DD/pRpx3Tal/qRW1SYwcIeQ0iq8li5CJHQgOC+FtPn2BVmuDtzUCgNnpCrbgtfEHqh+iWzxK+Tq6C+RQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3591,6 +3650,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -3670,6 +3733,9 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + typescript-fsm@1.6.0: + resolution: {integrity: sha512-wmDVbIZ/iyytlNOLdwyvLTdnArSZw48IwjbHuIcZo+DA2do27pOM+hZJsjPzcQ0ux/lzkZ1i1PPfP16GOGPYuQ==} + typescript@4.5.2: resolution: {integrity: sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==} engines: {node: '>=4.2.0'} @@ -3771,20 +3837,20 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -3913,6 +3979,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xstate@5.32.4: + resolution: {integrity: sha512-E5WtDB8DBs2ZWliz2Ry9XfbSZTbBRcK/cwefBot04qQ/L5SLP16xpnTDU4/ZFXuXFhNxi7JP2RhuoGwBnM+S4A==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4602,6 +4671,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@blazediff/core@1.9.1': {} + '@bufbuild/protobuf@1.10.1': {} '@bufbuild/protoc-gen-es@1.10.1(@bufbuild/protobuf@1.10.1)': @@ -5110,6 +5181,8 @@ snapshots: '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} + '@publint/pack@0.1.4': {} '@rollup/plugin-babel@7.0.0(@babel/core@7.29.7)(rollup@4.60.4)': @@ -5541,44 +5614,74 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitest/expect@4.1.7': + '@vitest/browser-playwright@4.1.10(playwright@1.61.1)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + playwright: 1.61.1 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))': + '@vitest/mocker@4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4) - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.7': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.7': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -6531,6 +6634,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7069,6 +7175,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -7227,6 +7335,16 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + possible-typed-array-names@1.1.0: optional: true @@ -7515,6 +7633,12 @@ snapshots: signal-exit@4.1.0: {} + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + size-limit@11.2.0: dependencies: bytes-iec: 3.1.1 @@ -7646,6 +7770,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -7755,6 +7881,8 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-fsm@1.6.0: {} + typescript@4.5.2: {} typescript@5.9.3: {} @@ -7841,15 +7969,15 @@ snapshots: tsx: 4.22.3 yaml: 2.8.4 - vitest@4.1.7(@types/node@25.6.0)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)): + vitest@4.1.10(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -7865,6 +7993,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 + '@vitest/browser-playwright': 4.1.10(playwright@1.61.1)(vite@7.3.5(@types/node@25.6.0)(jiti@2.4.2)(terser@5.39.2)(tsx@4.22.3)(yaml@2.8.4))(vitest@4.1.10) happy-dom: 20.9.0 jsdom: 26.1.0 transitivePeerDependencies: @@ -8008,6 +8137,8 @@ snapshots: xmlchars@2.2.0: {} + xstate@5.32.4: {} + yallist@3.1.1: {} yaml@2.8.4: {} diff --git a/src/api/SignalClient.e2e.test.ts b/src/api/SignalClient.e2e.test.ts new file mode 100644 index 0000000000..dc5d75eeeb --- /dev/null +++ b/src/api/SignalClient.e2e.test.ts @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, inject, it, vi } from 'vitest'; +import { LeaveRequest_Action, type LeaveRequest } from '@livekit/protocol'; +import { ConnectionErrorReason } from '../room/errors'; +import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient'; +import { createInvalidToken, createToken } from '../test/signalToken'; + +// Provided by src/test/signalServerSetup.ts (spawns the livekit-server test-server). +const serverUrl = inject('serverUrl'); +const unavailable = inject('e2eUnavailable'); + +const defaultOpts = (): SignalOptions => ({ + autoSubscribe: true, + maxRetries: 0, + e2eeEnabled: false, + websocketTimeout: 5_000, +}); + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +function withTimeout(p: Promise, ms: number, label: string): Promise { + return Promise.race([ + p, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), ms), + ), + ]); +} + +/** Resolves with the reason passed to onClose (register before the event can fire). */ +function captureClose(client: SignalClient): Promise { + return new Promise((resolve) => { + client.onClose = (reason) => resolve(reason); + }); +} + +/** Resolves with the LeaveRequest passed to onLeave. */ +function captureLeave(client: SignalClient): Promise { + return new Promise((resolve) => { + client.onLeave = (leave) => resolve(leave); + }); +} + +describe.skipIf(!!unavailable)('SignalClient e2e', () => { + // Behaviors that depend on the signal path (v0 legacy vs v1). + describe.each([ + { label: 'v0', useV0: true }, + { label: 'v1', useV0: false }, + ])('$label path', ({ useV0 }) => { + let client: SignalClient; + + beforeEach(() => { + client = new SignalClient(false); + }); + + afterEach(async () => { + await client.close().catch(() => {}); + }); + + /** Mint a token whose metadata selects the server behavior mode, then join. */ + const join = (mode: string, opts: SignalOptions = defaultOpts(), abortSignal?: AbortSignal) => + createToken({ signal: mode }).then((token) => + client.join(serverUrl, token, opts, abortSignal, useV0), + ); + + it('connects and reports CONNECTED with a join response', async () => { + const res = await join('happy'); + expect(res).toBeTruthy(); + expect(res.pingInterval).toBeGreaterThan(0); + expect(client.currentState).toBe(SignalConnectionState.CONNECTED); + }); + + it('stays connected past the ping timeout while the server pongs', async () => { + const onClose = vi.fn(); + await join('happy'); + client.onClose = onClose; + // Server ping timeout is 3s; a healthy pong loop must keep us alive. + await sleep(4_000); + expect(onClose).not.toHaveBeenCalled(); + expect(client.currentState).toBe(SignalConnectionState.CONNECTED); + }); + + it('closes with a ping timeout when the server stops ponging', async () => { + const closed = captureClose(client); + await join('no_pong'); + expect(client.currentState).toBe(SignalConnectionState.CONNECTED); + const reason = await withTimeout(closed, 8_000, 'onClose (ping timeout)'); + expect(reason).toContain('ping timeout'); + expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED); + }); + + it('surfaces an unexpected transport close while connected', async () => { + const closed = captureClose(client); + await join('close_when_connected'); + const reason = await withTimeout(closed, 5_000, 'onClose (transport closed)'); + expect(reason).toBeTruthy(); + expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED); + }); + + it('surfaces an abnormal socket drop (1006) while connected', async () => { + // Server sends the join, then drops the TCP connection with no close + // handshake -> the browser reports an abnormal 1006 closure, exercising + // WebSocketStream's error->close path (distinct from the clean 1011 above). + const closed = captureClose(client); + await join('drop_when_connected'); + expect(client.currentState).toBe(SignalConnectionState.CONNECTED); + const reason = await withTimeout(closed, 5_000, 'onClose (abnormal drop)'); + + // KNOWN BUG (documented, not desired): an abnormal 1006 close carries no + // reason, and handleOnClose's `closeInfo.reason ?? 'Unexpected WS error'` + // fallback only fires for null/undefined — not the empty string a 1006 + // yields. So onClose gets '' instead of a descriptive reason. It SHOULD + // report 'Unexpected WS error'. When the client is fixed, flip this to + // expect(reason).toBe('Unexpected WS error'); + expect(reason).toBe(''); // <-- buggy current behavior; see above + expect(client.currentState).toBe(SignalConnectionState.DISCONNECTED); + }); + + it('rejects the join when the socket closes before the first message', async () => { + // Upgrade succeeds, then the server closes the WS before sending a join. + // The client is still establishing, so it rejects rather than hanging. + const err = await join('close_before_join').then( + () => undefined, + (e) => e as Error, + ); + expect(err).toBeInstanceOf(Error); + expect((err as { reason?: ConnectionErrorReason }).reason).toBe(ConnectionErrorReason.InternalError); + }); + + it('delivers a server-initiated leave while connected', async () => { + const left = captureLeave(client); + await join('leave_when_connected'); + const leave = await withTimeout(left, 5_000, 'onLeave'); + expect(leave).toBeTruthy(); + expect(leave.action).toBe(LeaveRequest_Action.DISCONNECT); + }); + + it('forwards a non-DISCONNECT leave action verbatim', async () => { + // SignalClient does not branch on the leave action (RTCEngine does); it + // must pass it through unchanged. Guards the FSM swap's leave_received + // effect, which carries leaveAction. Uses RECONNECT to prove it's not + // defaulted to DISCONNECT. + const left = captureLeave(client); + const token = await createToken({ + signal: 'leave_when_connected', + leaveAction: LeaveRequest_Action.RECONNECT, + }); + await client.join(serverUrl, token, defaultOpts(), undefined, useV0); + const leave = await withTimeout(left, 5_000, 'onLeave'); + expect(leave.action).toBe(LeaveRequest_Action.RECONNECT); + }); + + it('rejects the join when a leave arrives as the first message', async () => { + const err = await join('leave_first_message').then( + () => undefined, + (e) => e as Error, + ); + expect(err).toBeInstanceOf(Error); + }); + + it('closes gracefully via close() without firing onClose', async () => { + const onClose = vi.fn(); + await join('happy'); + client.onClose = onClose; + await client.close(); + expect(client.isDisconnected).toBe(true); + expect(onClose).not.toHaveBeenCalled(); + }); + + it('reconnects (resume) back to CONNECTED', async () => { + await join('happy'); + const token = await createToken({ signal: 'happy' }); + await client.reconnect(serverUrl, token, 'RM_session'); + expect(client.currentState).toBe(SignalConnectionState.CONNECTED); + }); + + it('rejects a reconnect when a leave arrives as the first message', async () => { + // Reconnecting into a room that sends leave-first exercises the client's + // first-message validation while in RECONNECTING (path-independent: it + // doesn't rely on the mock detecting reconnect, which v1 hides inside the + // gzipped join_request the mock ignores). + await join('happy'); + const token = await createToken({ signal: 'leave_first_message' }); + const err = await client.reconnect(serverUrl, token, 'RM_session').then( + () => undefined, + (e) => e as Error, + ); + expect(err).toBeInstanceOf(Error); + }); + + // --- validate-endpoint classification ------------------------------- + it('classifies an invalid token as NotAllowed', async () => { + const token = await createInvalidToken(); + const err = await client + .join(serverUrl, token, defaultOpts(), undefined, useV0) + .catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.NotAllowed); + }); + + it('classifies room-not-found as NotAllowed', async () => { + const err = await join('room_not_found').catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.NotAllowed); + }); + + it('classifies a wrong-path 404 as ServiceNotFound', async () => { + const err = await join('validate_service_not_found').catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.ServiceNotFound); + }); + + it('surfaces a 5xx validate as a WebSocket error (ws error shadows 5xx→internal)', async () => { + // Current behavior: handleConnectionError only maps a 5xx to InternalError + // when the ws rejection is NOT a ConnectionError. WebSocketStream always + // rejects with one, so for a refused upgrade the WS error wins. Only + // 401/403/404 (handled before that check) override it. + const err = await join('validate_500').catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.WebSocket); + }); + }); + + // Path-independent transport-level failures (run once, via v0). + describe('transport failures', () => { + let client: SignalClient; + + beforeEach(() => { + client = new SignalClient(false); + }); + + afterEach(async () => { + await client.close().catch(() => {}); + }); + + it('classifies an unreachable server as ServerUnreachable', async () => { + const token = await createToken({ signal: 'happy' }); + // Nothing listening on this port -> ws fails, validate fetch fails. + const err = await client + .join('ws://127.0.0.1:59999', token, defaultOpts(), undefined, true) + .catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.ServerUnreachable); + }); + + it('cancels the connect when the AbortSignal fires mid-connection', async () => { + const controller = new AbortController(); + const token = await createToken({ signal: 'no_first_message' }); + const p = client.join(serverUrl, token, defaultOpts(), controller.signal, true); + setTimeout(() => controller.abort('user requested abort'), 300); + const err = await p.catch((e) => e); + expect(err.reason).toBe(ConnectionErrorReason.Cancelled); + }); + + it('hangs with no read-timeout when the socket opens but no first message arrives', async () => { + // FIXME KNOWN GAP (documented, not desired): wsTimeout guards only the upgrade + // and is cleared as soon as ws.opened resolves. If the server accepts the + // socket then stays silent, `await signalReader.read()` blocks forever — + // join() never settles and websocketTimeout does NOT apply post-upgrade. + const controller = new AbortController(); + const token = await createToken({ signal: 'no_first_message' }); + const joinPromise = client.join( + serverUrl, + token, + { ...defaultOpts(), websocketTimeout: 1_000 }, + controller.signal, + true, + ); + + const STILL_PENDING = Symbol('still-pending'); + const outcome = await Promise.race([ + joinPromise.then( + () => 'resolved', + () => 'rejected', + ), + // Well past websocketTimeout (1s): a real read-timeout would have fired. + new Promise((resolve) => + setTimeout(() => resolve(STILL_PENDING), 3_000), + ), + ]); + expect(outcome).toBe(STILL_PENDING); + + // Cleanup: abort so the dangling join settles instead of leaking. + controller.abort('test cleanup'); + await joinPromise.catch(() => {}); + }); + }); +}); diff --git a/src/test/signalServerSetup.ts b/src/test/signalServerSetup.ts new file mode 100644 index 0000000000..44179399bc --- /dev/null +++ b/src/test/signalServerSetup.ts @@ -0,0 +1,147 @@ +import { type ChildProcess, execFileSync, spawn } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { createServer } from 'node:net'; +import { join } from 'node:path'; +import { createToken } from './signalToken'; + +/** + * vitest globalSetup for the browser signal e2e suite. It provides the tests a + * ws:// base URL for the LiveKit mock test-server, via one of two paths: + * + * - LK_TEST_SERVER_URL — connect to an already-running server (CI runs the + * published test-server Docker image as a service; see .github/workflows/e2e.yaml). + * - LK_SERVER_DIR — a local livekit-server checkout: build cmd/test-server + * and spawn it on an ephemeral port (dev convenience). + * + * If neither is set (or the toolchain is unavailable) the suite is skipped. + */ + +const HOST = '127.0.0.1'; + +let child: ChildProcess | undefined; + +function getFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = createServer(); + srv.unref(); + srv.on('error', reject); + srv.listen(0, HOST, () => { + const addr = srv.address(); + const port = typeof addr === 'object' && addr ? addr.port : 0; + srv.close(() => resolve(port)); + }); + }); +} + +// Poll the validate endpoint (from Node — no CORS here) until it answers 200. +async function waitReady(serverUrl: string): Promise { + const token = await createToken({ signal: 'happy' }); + const httpBase = serverUrl.replace(/^ws/, 'http'); + const validateUrl = `${httpBase}/rtc/validate?access_token=${encodeURIComponent(token)}`; + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + try { + if ((await fetch(validateUrl)).status === 200) { + return true; + } + } catch { + // not up yet + } + await new Promise((r) => setTimeout(r, 200)); + } + return false; +} + +export default async function setup(project: { provide: (name: string, value: unknown) => void }) { + const provide = (serverUrl: string) => { + project.provide('serverUrl', serverUrl); + project.provide('e2eUnavailable', ''); + }; + const skip = (reason: string) => { + // eslint-disable-next-line no-console + console.warn(`\n[e2e] signal-connection e2e suite SKIPPED: ${reason}\n`); + project.provide('serverUrl', ''); + project.provide('e2eUnavailable', reason); + return () => {}; + }; + + // CI / Docker: connect to an externally-managed server. + const externalUrl = process.env.LK_TEST_SERVER_URL; + if (externalUrl) { + if (!(await waitReady(externalUrl))) { + return skip(`test-server at ${externalUrl} did not become ready`); + } + provide(externalUrl); + return () => {}; // container lifecycle is owned by CI + } + + // Local dev: build + spawn cmd/test-server from a livekit-server checkout. + const serverDir = process.env.LK_SERVER_DIR; + if (!serverDir || !existsSync(serverDir)) { + return skip( + 'set LK_TEST_SERVER_URL (a running server) or LK_SERVER_DIR (a livekit-server checkout)', + ); + } + + const binary = join(serverDir, 'bin', 'test-server'); + if (!process.env.LK_E2E_SKIP_BUILD) { + try { + execFileSync('go', ['build', '-o', 'bin/test-server', './cmd/test-server'], { + cwd: serverDir, + stdio: 'pipe', + }); + } catch (e) { + return skip(`failed to build test-server: ${(e as Error).message}`); + } + } + if (!existsSync(binary)) { + return skip(`test-server binary missing at ${binary}`); + } + + // Fresh ephemeral port each run so a leftover server from a crashed run can + // never be mistaken for ours (vitest browser-mode teardown is unreliable, so + // a spawned server may outlive the run; the ephemeral port makes that + // harmless, at the cost of leaving at most one idle process per run). + const port = Number(process.env.LK_TEST_SERVER_PORT) || (await getFreePort()); + const serverUrl = `ws://${HOST}:${port}`; + + child = spawn(binary, ['--ports', String(port), '--bind', HOST], { + cwd: serverDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let serverLog = ''; + child.stdout?.on('data', (d: unknown) => { + serverLog += String(d); + }); + child.stderr?.on('data', (d: unknown) => { + serverLog += String(d); + }); + child.stdout?.unref?.(); + child.stderr?.unref?.(); + child.unref(); + + const kill = () => { + try { + child?.kill('SIGKILL'); + } catch { + // already gone + } + }; + process.once('exit', kill); + process.once('SIGINT', () => { + kill(); + process.exit(130); + }); + process.once('SIGTERM', () => { + kill(); + process.exit(143); + }); + + if (!(await waitReady(serverUrl))) { + kill(); + return skip(`test-server did not become ready on ${serverUrl}\n--- server output ---\n${serverLog}`); + } + + provide(serverUrl); + return kill; +} diff --git a/src/test/signalToken.ts b/src/test/signalToken.ts new file mode 100644 index 0000000000..5d89d34bef --- /dev/null +++ b/src/test/signalToken.ts @@ -0,0 +1,59 @@ +import { SignJWT } from 'jose'; + +/** + * Mint a LiveKit access token for the mock test-server (HS256, dev secret). + * + * The mock's signal behavior mode is selected via a participant attribute + * under the reserved key `lk.mock` (dot notation, matching LiveKit's internal + * attribute convention), whose value is the JSON control object + * `{"signal":""}` — mirroring the X-Lk-Mock header protocol. Attributes + * are a standard AccessToken field (a string map), so a dedicated `lk-mock` + * key coexists with any real metadata/attributes and needs no bespoke token + * construction. The room name is just a room; it no longer encodes behavior. + */ +export interface TokenOptions { + /** Mock behavior mode (e.g. 'no_pong'); omitted → the mock defaults to 'happy'. */ + signal?: string; + /** LeaveRequest action the leave modes should send (0=DISCONNECT,1=RESUME,2=RECONNECT). */ + leaveAction?: number; + room?: string; + identity?: string; + apiKey?: string; + secret?: string; + ttlSeconds?: number; +} + +export async function createToken(opts: TokenOptions = {}): Promise { + const { + signal, + leaveAction, + room = 'e2e-room', + identity = `test-${Math.random().toString(36).slice(2, 8)}`, + apiKey = 'devkey', + secret = 'secret', + ttlSeconds = 600, + } = opts; + const key = new TextEncoder().encode(secret); + const payload: Record = { + video: { room, roomJoin: true, canPublish: true, canSubscribe: true, canPublishData: true }, + }; + if (signal) { + const control: Record = { signal }; + if (leaveAction !== undefined) { + control.leaveAction = leaveAction; + } + payload.attributes = { 'lk.mock': JSON.stringify(control) }; + } + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuer(apiKey) + .setSubject(identity) + .setIssuedAt() + .setExpirationTime(`${ttlSeconds}s`) + .sign(key); +} + +/** A syntactically-valid token signed with the WRONG secret — for 401 tests. */ +export async function createInvalidToken(): Promise { + return createToken({ secret: 'not-the-secret' }); +} diff --git a/src/test/vitest-context.d.ts b/src/test/vitest-context.d.ts new file mode 100644 index 0000000000..725edc3aba --- /dev/null +++ b/src/test/vitest-context.d.ts @@ -0,0 +1,10 @@ +import 'vitest'; + +declare module 'vitest' { + interface ProvidedContext { + /** ws:// base URL of the spawned mock test-server, or '' when unavailable. */ + serverUrl: string; + /** Non-empty reason string when the e2e suite must be skipped. */ + e2eUnavailable: string; + } +} diff --git a/tsconfig.json b/tsconfig.json index 4486b144b4..edcc6cb45e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ "ignoreDeprecations": "5.0", "plugins": [{ "name": "@livekit/throws-transformer" }] }, - "exclude": ["dist", "**/*.test.ts", "test/**", "src/room/token-source/test-tokens.ts"], + "exclude": ["dist", "**/*.test.ts", "test/**", "src/room/token-source/test-tokens.ts", "src/test/signalServerSetup.ts"], "include": ["src/**/*.ts"], "typedocOptions": { "entryPoints": ["src/index.ts"], diff --git a/vite.config.mjs b/vite.config.mjs index a5fd71986a..1015d4156c 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -41,5 +41,8 @@ export default defineConfig({ }, test: { environment: 'happy-dom', + // e2e tests need a real server + node WebSocket; run them via the + // dedicated `pnpm test:e2e` config (vitest.e2e.config.mts), not the unit run. + exclude: ['**/node_modules/**', '**/*.e2e.test.ts'], }, }); diff --git a/vitest.e2e.config.mts b/vitest.e2e.config.mts new file mode 100644 index 0000000000..68f1bf6f7e --- /dev/null +++ b/vitest.e2e.config.mts @@ -0,0 +1,24 @@ +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +// End-to-end signal-connection tests. Unlike the unit suite (happy-dom, mocked +// transport), these run in a REAL browser (Chromium via Playwright) so they +// exercise the actual browser WebSocket + fetch the SDK ships against, driving +// a live mock server (../livekit-server/cmd/test-server) spawned by globalSetup. +export default defineConfig({ + test: { + include: ['src/**/*.e2e.test.ts'], + // globalSetup runs in Node (spawns the Go mock); tests run in the browser. + globalSetup: ['./src/test/signalServerSetup.ts'], + testTimeout: 20_000, + hookTimeout: 120_000, + // One browser context; scenarios isolate via unique-per-mode room names. + fileParallelism: false, + browser: { + enabled: true, + provider: playwright(), + headless: true, + instances: [{ browser: 'chromium' }], + }, + }, +}); From 7084276cd194cc75ba333eb76d0dbe0de601eee2 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:22:57 +0200 Subject: [PATCH 2/9] run action for testing --- .github/workflows/e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0c642080ad..d495c31de0 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -2,7 +2,7 @@ name: E2E on: push: - branches: [main] + branches: [main, "lukas/signal-e2e"] pull_request: branches: [main] From d0e13a69ba1227550a31573d9893f46dab24309e Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:25:00 +0200 Subject: [PATCH 3/9] remove action test branch --- .github/workflows/e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index d495c31de0..0c642080ad 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -2,7 +2,7 @@ name: E2E on: push: - branches: [main, "lukas/signal-e2e"] + branches: [main] pull_request: branches: [main] From ec91f6430c6cac4bdca15134edc91f5fcb2b68a4 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:26:51 +0200 Subject: [PATCH 4/9] lint --- src/api/SignalClient.e2e.test.ts | 8 +++++--- src/test/signalServerSetup.ts | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/SignalClient.e2e.test.ts b/src/api/SignalClient.e2e.test.ts index dc5d75eeeb..ed64a56f55 100644 --- a/src/api/SignalClient.e2e.test.ts +++ b/src/api/SignalClient.e2e.test.ts @@ -1,8 +1,8 @@ +import { type LeaveRequest, LeaveRequest_Action } from '@livekit/protocol'; import { afterEach, beforeEach, describe, expect, inject, it, vi } from 'vitest'; -import { LeaveRequest_Action, type LeaveRequest } from '@livekit/protocol'; import { ConnectionErrorReason } from '../room/errors'; -import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient'; import { createInvalidToken, createToken } from '../test/signalToken'; +import { SignalClient, SignalConnectionState, type SignalOptions } from './SignalClient'; // Provided by src/test/signalServerSetup.ts (spawns the livekit-server test-server). const serverUrl = inject('serverUrl'); @@ -123,7 +123,9 @@ describe.skipIf(!!unavailable)('SignalClient e2e', () => { (e) => e as Error, ); expect(err).toBeInstanceOf(Error); - expect((err as { reason?: ConnectionErrorReason }).reason).toBe(ConnectionErrorReason.InternalError); + expect((err as { reason?: ConnectionErrorReason }).reason).toBe( + ConnectionErrorReason.InternalError, + ); }); it('delivers a server-initiated leave while connected', async () => { diff --git a/src/test/signalServerSetup.ts b/src/test/signalServerSetup.ts index 44179399bc..b0cdead3a4 100644 --- a/src/test/signalServerSetup.ts +++ b/src/test/signalServerSetup.ts @@ -139,7 +139,9 @@ export default async function setup(project: { provide: (name: string, value: un if (!(await waitReady(serverUrl))) { kill(); - return skip(`test-server did not become ready on ${serverUrl}\n--- server output ---\n${serverLog}`); + return skip( + `test-server did not become ready on ${serverUrl}\n--- server output ---\n${serverLog}`, + ); } provide(serverUrl); From e82f68e7cf1a19ad7f9f4d5919240c18d8c8a3a2 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:33:16 +0200 Subject: [PATCH 5/9] cleaner unref handling --- src/test/signalServerSetup.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/signalServerSetup.ts b/src/test/signalServerSetup.ts index b0cdead3a4..49bb767a04 100644 --- a/src/test/signalServerSetup.ts +++ b/src/test/signalServerSetup.ts @@ -1,6 +1,6 @@ import { type ChildProcess, execFileSync, spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; -import { createServer } from 'node:net'; +import { Socket, createServer } from 'node:net'; import { join } from 'node:path'; import { createToken } from './signalToken'; @@ -116,8 +116,10 @@ export default async function setup(project: { provide: (name: string, value: un child.stderr?.on('data', (d: unknown) => { serverLog += String(d); }); - child.stdout?.unref?.(); - child.stderr?.unref?.(); + // With stdio: 'pipe' these are net.Sockets (typed as Readable, which lacks unref); + // unref the pipe handles so their 'data' listeners don't keep the parent alive. + if (child.stdout instanceof Socket) child.stdout.unref(); + if (child.stderr instanceof Socket) child.stderr.unref(); child.unref(); const kill = () => { From 5613db88f7d8730940d14b2b6d5ad60b8d087f52 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:34:53 +0200 Subject: [PATCH 6/9] clean up deps --- package.json | 4 +- pnpm-lock.yaml | 227 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 207 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index c36aa62397..82cb436a9b 100644 --- a/package.json +++ b/package.json @@ -80,9 +80,7 @@ "sdp-transform": "^2.15.0", "tslib": "2.8.1", "typed-emitter": "^2.1.0", - "typescript-fsm": "^1.6.0", - "webrtc-adapter": "9.0.6", - "xstate": "^5.32.4" + "webrtc-adapter": "9.0.6" }, "peerDependencies": { "@types/dom-mediacapture-record": "^1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c958894e1d..def07b37b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,15 +35,9 @@ importers: typed-emitter: specifier: ^2.1.0 version: 2.1.0 - typescript-fsm: - specifier: ^1.6.0 - version: 1.6.0 webrtc-adapter: specifier: 9.0.6 version: 9.0.6 - xstate: - specifier: ^5.32.4 - version: 5.32.4 devDependencies: '@babel/core': specifier: 7.29.7 @@ -1648,6 +1642,126 @@ packages: resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.1-rc': + resolution: {integrity: sha512-oqq2ZfEJ7BQuufcC3QBQndZLPNyamYNHLao8lKRBeeSkZKypBqxPSgkzrcFZtbYcIaBvpiyUnQP9MT7DEYHWbw==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.1-rc': + resolution: {integrity: sha512-Slc0yTftT2F/uGDmtPst8ijydneL6uZaLEyr2UjahxZpbhTjHFBJ5agXtVz/TL4A+ldxzjzj+E8QtLZlh/5mXw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.1-rc': + resolution: {integrity: sha512-h68iFW/LbA1/BsGgSRGFw981/3s1f/rY27YrmeZNuN+ly7dI+fiDduwT9ZT9866x2onoKNRq7PTyxSKyKDzfAQ==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.1-rc': + resolution: {integrity: sha512-DE+ppd8Ix2c6OMuRkKY4PJ4hngMGJ9M95OQUP17p9xL/1IKXof7npIeuusMN/bgL5o5JzMfSGh+N+5scTYRg0Q==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.1-rc': + resolution: {integrity: sha512-ST1ozHMw0u+CLOnWkcTyWDMV4Qn9osZ6fd1V1lnKDM1t0hZIp81mdGpdHxyHJjd7jdGrb6Gb/QXcZ1uqZ0t5zw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.1-rc': + resolution: {integrity: sha512-N46pRihK3t5zD5MUtTQcdmQUqr1WI4U2nxno1gLwOtRSsB4krFkRjPHcQNG7h2DtRkX64rQiReX6WKwg2wprMA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.1-rc': + resolution: {integrity: sha512-gHmHwT5Naq5CKM8g9bbaGeEpnwQEvWCLn3fwP4K2m61VQdDKkPk0Dhab/OoZ4LV2SrMddmclYXTzpyg23YGt5g==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.1-rc': + resolution: {integrity: sha512-G17Sao312rgiPBTh2F4nOpLpa3CcnBSaNhqNghZk2LNhnsp1RaMO5HMq2me21gqu9xLpc6CIgHtOzU6JBgNlfg==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.1-rc': + resolution: {integrity: sha512-0FQspOb5UsQ4tQKvWgUO3pS9OIWkP7/8dPRWq+CRazJUeQZ4LBjtYK52jg5iIOrvItrVl2CwvRtrU3/9OihwJg==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.1-rc': + resolution: {integrity: sha512-SUmwfVBEv6A2Ld0eWfcvW0FqrgemfQL8jFGOmV1qYxsDqumjE5DekHXqbstgmbE4SHr4rrjHjvmuGCY+kTH/vw==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.1-rc': + resolution: {integrity: sha512-rxeqnNnGiYzv/LlPHi/3+4p0ooR1cNJLjRIHXKovtiVmxXGJq6gtw8VSpbHuWPekyFMXgIAoLCZN0SQ51rAALQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.1-rc': + resolution: {integrity: sha512-RYWCHCiPypxajdRHM2CNK/eM22e4Ex5TTjV2pXf7PTtBowGr0xX8i8kIMknyZS0LX2QfleYHouaoMVsFDSle3g==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.1-rc': + resolution: {integrity: sha512-PfLJSu0JzroDkqw2m4nqflPEcn8yev0m/vHFQlY9EzHorzjR6QG0wL8AJHvnD1e6h1s76AZngJ5u+z1K/D/HKw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.1-rc': + resolution: {integrity: sha512-FfbPxH3dTfp8yVIaNM7bdWTixXuyxpzoemluqcqMROSIz+ImpCG3Q9HO9Ptzp9/giv+P9YYEnCMSXh61migj2w==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.1-rc': + resolution: {integrity: sha512-FzdTfSzhRYb6hlav6K3cI5RVgcvCTvNAu/vc+t7B6AmZkThQ+t/1ntnvT5fnHmY1Az2RIBw7/b+qtCEG61HJTQ==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.1-rc': + resolution: {integrity: sha512-PQGhlxfNig+0YQ9Wwzd0USPBkt6w/ZqkBQWsU7G/0JkTzunJel+jSWwhKw4947pak/m7hGSeYiI04xReDLGZww==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.1-rc': + resolution: {integrity: sha512-WJ7NYgO2mHmLUkI/tZ+hl8lFd26QPJqO8ONOHNuYbdsybLvSB6B6sep222JIVrOfPRDGvFinbGGB+l3m1FWRWA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.1-rc': + resolution: {integrity: sha512-UYjDeUxd765V9qcwlUPk4pEXyL0i3G76CJm9baK4i99u1pGO1psf3nXDw4MMmElVOPvGbZag99ZR/O59E2OX6w==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.1-rc': + resolution: {integrity: sha512-KzXzFSXZOm7zvEt2Aw0MsB2LbTL88znAiVqTDNAOHdlEb7brgmUQh/X2wM/8Be+N0fjEqWKl65cBKNwpWEbJiw==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.1-rc': + resolution: {integrity: sha512-98R3+OqDr/r0/PLWEoXu88AE0lGVLNd335Ew8ONgzK1JWkNs4ou/5BGt3Or1ij4iXjH+c7PRL+jFjCbtWze+EA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@typescript/vfs@1.6.1': resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} peerDependencies: @@ -3733,9 +3847,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript-fsm@1.6.0: - resolution: {integrity: sha512-wmDVbIZ/iyytlNOLdwyvLTdnArSZw48IwjbHuIcZo+DA2do27pOM+hZJsjPzcQ0ux/lzkZ1i1PPfP16GOGPYuQ==} - typescript@4.5.2: resolution: {integrity: sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==} engines: {node: '>=4.2.0'} @@ -3746,9 +3857,9 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@6.0.0-dev.20260416: - resolution: {integrity: sha512-64CCow5rQc6+oBAPqLtvZ8f3iz4WVCLwMVkg6/kOfMQlGshVOMkBhCXn+MMP4SX4Xtnz48VpD98FkptirMEuEQ==} - engines: {node: '>=14.17'} + typescript@7.0.1-rc: + resolution: {integrity: sha512-drEP77wK7CCDlPfXZH4e008UUQOsw1DFmHmZOZjuNA+yoDLLnSNMZRXi90NbV/1LVo7SbNLq1bs3jjvk49TEqQ==} + engines: {node: '>=16.20.0'} hasBin: true uc.micro@2.1.0: @@ -3979,9 +4090,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xstate@5.32.4: - resolution: {integrity: sha512-E5WtDB8DBs2ZWliz2Ry9XfbSZTbBRcK/cwefBot04qQ/L5SLP16xpnTDU4/ZFXuXFhNxi7JP2RhuoGwBnM+S4A==} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -5537,6 +5645,66 @@ snapshots: '@typescript-eslint/types': 8.59.4 eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.1-rc': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-darwin-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-arm@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-loong64@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-s390x@7.0.1-rc': + optional: true + + '@typescript/typescript-linux-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-sunos-x64@7.0.1-rc': + optional: true + + '@typescript/typescript-win32-arm64@7.0.1-rc': + optional: true + + '@typescript/typescript-win32-x64@7.0.1-rc': + optional: true + '@typescript/vfs@1.6.1(typescript@4.5.2)': dependencies: debug: 4.4.3 @@ -6098,7 +6266,7 @@ snapshots: dependencies: semver: 7.8.2 shelljs: 0.8.5 - typescript: 6.0.0-dev.20260416 + typescript: 7.0.1-rc dunder-proto@1.0.1: dependencies: @@ -7881,13 +8049,32 @@ snapshots: transitivePeerDependencies: - supports-color - typescript-fsm@1.6.0: {} - typescript@4.5.2: {} typescript@5.9.3: {} - typescript@6.0.0-dev.20260416: {} + typescript@7.0.1-rc: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.1-rc + '@typescript/typescript-darwin-arm64': 7.0.1-rc + '@typescript/typescript-darwin-x64': 7.0.1-rc + '@typescript/typescript-freebsd-arm64': 7.0.1-rc + '@typescript/typescript-freebsd-x64': 7.0.1-rc + '@typescript/typescript-linux-arm': 7.0.1-rc + '@typescript/typescript-linux-arm64': 7.0.1-rc + '@typescript/typescript-linux-loong64': 7.0.1-rc + '@typescript/typescript-linux-mips64el': 7.0.1-rc + '@typescript/typescript-linux-ppc64': 7.0.1-rc + '@typescript/typescript-linux-riscv64': 7.0.1-rc + '@typescript/typescript-linux-s390x': 7.0.1-rc + '@typescript/typescript-linux-x64': 7.0.1-rc + '@typescript/typescript-netbsd-arm64': 7.0.1-rc + '@typescript/typescript-netbsd-x64': 7.0.1-rc + '@typescript/typescript-openbsd-arm64': 7.0.1-rc + '@typescript/typescript-openbsd-x64': 7.0.1-rc + '@typescript/typescript-sunos-x64': 7.0.1-rc + '@typescript/typescript-win32-arm64': 7.0.1-rc + '@typescript/typescript-win32-x64': 7.0.1-rc uc.micro@2.1.0: {} @@ -8137,8 +8324,6 @@ snapshots: xmlchars@2.2.0: {} - xstate@5.32.4: {} - yallist@3.1.1: {} yaml@2.8.4: {} From 597b0779ca53c499f948ee520d26eb630a7116f5 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:35:16 +0200 Subject: [PATCH 7/9] cleanup --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 82cb436a9b..2847c2cd21 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "build-docs": "typedoc && mkdir -p docs/assets/github && cp .github/*.png docs/assets/github/ && find docs -name '*.html' -type f -exec sed -i.bak 's|=\"/.github/|=\"assets/github/|g' {} + && find docs -name '*.bak' -delete", "proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto", "examples:demo": "vite examples/demo -c vite.config.mjs", - "examples:signal-fsm": "vite examples/signal-connection-fsm -c vite.config.mjs", "dev": "pnpm examples:demo", "lint": "eslint src", "test": "vitest run src", From 8c66b0b47281f259918ea667186abd35ca8467d2 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 16:55:46 +0200 Subject: [PATCH 8/9] use canonical TS version --- package.json | 2 +- pnpm-lock.yaml | 221 +++----------------------------------------- pnpm-workspace.yaml | 8 ++ 3 files changed, 20 insertions(+), 211 deletions(-) diff --git a/package.json b/package.json index 2847c2cd21..b9326a37e1 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "tsx": "^4.21.0", "typedoc": "0.28.19", "typedoc-plugin-no-inherit": "1.6.1", - "typescript": "5.9.3", + "typescript": "catalog:", "typescript-eslint": "^8.47.0", "vite": "7.3.5", "vitest": "^4.1.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index def07b37b5..247d0397cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,15 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + typescript: + specifier: 5.9.3 + version: 5.9.3 + +overrides: + downlevel-dts>typescript: 5.9.3 + importers: .: @@ -172,7 +181,7 @@ importers: specifier: 1.6.1 version: 1.6.1(typedoc@0.28.19(typescript@5.9.3)) typescript: - specifier: 5.9.3 + specifier: 'catalog:' version: 5.9.3 typescript-eslint: specifier: ^8.47.0 @@ -1642,126 +1651,6 @@ packages: resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/typescript-aix-ppc64@7.0.1-rc': - resolution: {integrity: sha512-oqq2ZfEJ7BQuufcC3QBQndZLPNyamYNHLao8lKRBeeSkZKypBqxPSgkzrcFZtbYcIaBvpiyUnQP9MT7DEYHWbw==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [aix] - - '@typescript/typescript-darwin-arm64@7.0.1-rc': - resolution: {integrity: sha512-Slc0yTftT2F/uGDmtPst8ijydneL6uZaLEyr2UjahxZpbhTjHFBJ5agXtVz/TL4A+ldxzjzj+E8QtLZlh/5mXw==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/typescript-darwin-x64@7.0.1-rc': - resolution: {integrity: sha512-h68iFW/LbA1/BsGgSRGFw981/3s1f/rY27YrmeZNuN+ly7dI+fiDduwT9ZT9866x2onoKNRq7PTyxSKyKDzfAQ==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/typescript-freebsd-arm64@7.0.1-rc': - resolution: {integrity: sha512-DE+ppd8Ix2c6OMuRkKY4PJ4hngMGJ9M95OQUP17p9xL/1IKXof7npIeuusMN/bgL5o5JzMfSGh+N+5scTYRg0Q==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [freebsd] - - '@typescript/typescript-freebsd-x64@7.0.1-rc': - resolution: {integrity: sha512-ST1ozHMw0u+CLOnWkcTyWDMV4Qn9osZ6fd1V1lnKDM1t0hZIp81mdGpdHxyHJjd7jdGrb6Gb/QXcZ1uqZ0t5zw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [freebsd] - - '@typescript/typescript-linux-arm64@7.0.1-rc': - resolution: {integrity: sha512-N46pRihK3t5zD5MUtTQcdmQUqr1WI4U2nxno1gLwOtRSsB4krFkRjPHcQNG7h2DtRkX64rQiReX6WKwg2wprMA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/typescript-linux-arm@7.0.1-rc': - resolution: {integrity: sha512-gHmHwT5Naq5CKM8g9bbaGeEpnwQEvWCLn3fwP4K2m61VQdDKkPk0Dhab/OoZ4LV2SrMddmclYXTzpyg23YGt5g==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/typescript-linux-loong64@7.0.1-rc': - resolution: {integrity: sha512-G17Sao312rgiPBTh2F4nOpLpa3CcnBSaNhqNghZk2LNhnsp1RaMO5HMq2me21gqu9xLpc6CIgHtOzU6JBgNlfg==} - engines: {node: '>=16.20.0'} - cpu: [loong64] - os: [linux] - - '@typescript/typescript-linux-mips64el@7.0.1-rc': - resolution: {integrity: sha512-0FQspOb5UsQ4tQKvWgUO3pS9OIWkP7/8dPRWq+CRazJUeQZ4LBjtYK52jg5iIOrvItrVl2CwvRtrU3/9OihwJg==} - engines: {node: '>=16.20.0'} - cpu: [mips64el] - os: [linux] - - '@typescript/typescript-linux-ppc64@7.0.1-rc': - resolution: {integrity: sha512-SUmwfVBEv6A2Ld0eWfcvW0FqrgemfQL8jFGOmV1qYxsDqumjE5DekHXqbstgmbE4SHr4rrjHjvmuGCY+kTH/vw==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [linux] - - '@typescript/typescript-linux-riscv64@7.0.1-rc': - resolution: {integrity: sha512-rxeqnNnGiYzv/LlPHi/3+4p0ooR1cNJLjRIHXKovtiVmxXGJq6gtw8VSpbHuWPekyFMXgIAoLCZN0SQ51rAALQ==} - engines: {node: '>=16.20.0'} - cpu: [riscv64] - os: [linux] - - '@typescript/typescript-linux-s390x@7.0.1-rc': - resolution: {integrity: sha512-RYWCHCiPypxajdRHM2CNK/eM22e4Ex5TTjV2pXf7PTtBowGr0xX8i8kIMknyZS0LX2QfleYHouaoMVsFDSle3g==} - engines: {node: '>=16.20.0'} - cpu: [s390x] - os: [linux] - - '@typescript/typescript-linux-x64@7.0.1-rc': - resolution: {integrity: sha512-PfLJSu0JzroDkqw2m4nqflPEcn8yev0m/vHFQlY9EzHorzjR6QG0wL8AJHvnD1e6h1s76AZngJ5u+z1K/D/HKw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/typescript-netbsd-arm64@7.0.1-rc': - resolution: {integrity: sha512-FfbPxH3dTfp8yVIaNM7bdWTixXuyxpzoemluqcqMROSIz+ImpCG3Q9HO9Ptzp9/giv+P9YYEnCMSXh61migj2w==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [netbsd] - - '@typescript/typescript-netbsd-x64@7.0.1-rc': - resolution: {integrity: sha512-FzdTfSzhRYb6hlav6K3cI5RVgcvCTvNAu/vc+t7B6AmZkThQ+t/1ntnvT5fnHmY1Az2RIBw7/b+qtCEG61HJTQ==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [netbsd] - - '@typescript/typescript-openbsd-arm64@7.0.1-rc': - resolution: {integrity: sha512-PQGhlxfNig+0YQ9Wwzd0USPBkt6w/ZqkBQWsU7G/0JkTzunJel+jSWwhKw4947pak/m7hGSeYiI04xReDLGZww==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [openbsd] - - '@typescript/typescript-openbsd-x64@7.0.1-rc': - resolution: {integrity: sha512-WJ7NYgO2mHmLUkI/tZ+hl8lFd26QPJqO8ONOHNuYbdsybLvSB6B6sep222JIVrOfPRDGvFinbGGB+l3m1FWRWA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [openbsd] - - '@typescript/typescript-sunos-x64@7.0.1-rc': - resolution: {integrity: sha512-UYjDeUxd765V9qcwlUPk4pEXyL0i3G76CJm9baK4i99u1pGO1psf3nXDw4MMmElVOPvGbZag99ZR/O59E2OX6w==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [sunos] - - '@typescript/typescript-win32-arm64@7.0.1-rc': - resolution: {integrity: sha512-KzXzFSXZOm7zvEt2Aw0MsB2LbTL88znAiVqTDNAOHdlEb7brgmUQh/X2wM/8Be+N0fjEqWKl65cBKNwpWEbJiw==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/typescript-win32-x64@7.0.1-rc': - resolution: {integrity: sha512-98R3+OqDr/r0/PLWEoXu88AE0lGVLNd335Ew8ONgzK1JWkNs4ou/5BGt3Or1ij4iXjH+c7PRL+jFjCbtWze+EA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - '@typescript/vfs@1.6.1': resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} peerDependencies: @@ -3857,11 +3746,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@7.0.1-rc: - resolution: {integrity: sha512-drEP77wK7CCDlPfXZH4e008UUQOsw1DFmHmZOZjuNA+yoDLLnSNMZRXi90NbV/1LVo7SbNLq1bs3jjvk49TEqQ==} - engines: {node: '>=16.20.0'} - hasBin: true - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -5645,66 +5529,6 @@ snapshots: '@typescript-eslint/types': 8.59.4 eslint-visitor-keys: 5.0.1 - '@typescript/typescript-aix-ppc64@7.0.1-rc': - optional: true - - '@typescript/typescript-darwin-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-darwin-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-freebsd-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-freebsd-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-arm@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-loong64@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-mips64el@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-ppc64@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-riscv64@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-s390x@7.0.1-rc': - optional: true - - '@typescript/typescript-linux-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-netbsd-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-netbsd-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-openbsd-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-openbsd-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-sunos-x64@7.0.1-rc': - optional: true - - '@typescript/typescript-win32-arm64@7.0.1-rc': - optional: true - - '@typescript/typescript-win32-x64@7.0.1-rc': - optional: true - '@typescript/vfs@1.6.1(typescript@4.5.2)': dependencies: debug: 4.4.3 @@ -6266,7 +6090,7 @@ snapshots: dependencies: semver: 7.8.2 shelljs: 0.8.5 - typescript: 7.0.1-rc + typescript: 5.9.3 dunder-proto@1.0.1: dependencies: @@ -8053,29 +7877,6 @@ snapshots: typescript@5.9.3: {} - typescript@7.0.1-rc: - optionalDependencies: - '@typescript/typescript-aix-ppc64': 7.0.1-rc - '@typescript/typescript-darwin-arm64': 7.0.1-rc - '@typescript/typescript-darwin-x64': 7.0.1-rc - '@typescript/typescript-freebsd-arm64': 7.0.1-rc - '@typescript/typescript-freebsd-x64': 7.0.1-rc - '@typescript/typescript-linux-arm': 7.0.1-rc - '@typescript/typescript-linux-arm64': 7.0.1-rc - '@typescript/typescript-linux-loong64': 7.0.1-rc - '@typescript/typescript-linux-mips64el': 7.0.1-rc - '@typescript/typescript-linux-ppc64': 7.0.1-rc - '@typescript/typescript-linux-riscv64': 7.0.1-rc - '@typescript/typescript-linux-s390x': 7.0.1-rc - '@typescript/typescript-linux-x64': 7.0.1-rc - '@typescript/typescript-netbsd-arm64': 7.0.1-rc - '@typescript/typescript-netbsd-x64': 7.0.1-rc - '@typescript/typescript-openbsd-arm64': 7.0.1-rc - '@typescript/typescript-openbsd-x64': 7.0.1-rc - '@typescript/typescript-sunos-x64': 7.0.1-rc - '@typescript/typescript-win32-arm64': 7.0.1-rc - '@typescript/typescript-win32-x64': 7.0.1-rc - uc.micro@2.1.0: {} unbox-primitive@1.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d55410c3dc..c74617fce0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,11 @@ +catalog: + typescript: 5.9.3 + +# downlevel-dts depends on `typescript@next`, which now resolves to the TS 7 native +# preview (no `.` export -> ERR_PACKAGE_PATH_NOT_EXPORTED). Pin it to our canonical TS. +overrides: + downlevel-dts>typescript: 'catalog:' + minimumReleaseAge: 2880 minimumReleaseAgeExclude: - '@livekit/*' From 3e9d4a1cd58390a0e6d330835bbb74844ce11dd5 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 9 Jul 2026 17:02:08 +0200 Subject: [PATCH 9/9] fix config --- vite.config.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vite.config.mjs b/vite.config.mjs index 1015d4156c..7cfadd307f 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -3,6 +3,7 @@ import { babel } from '@rollup/plugin-babel'; import dns from 'dns'; import { resolve } from 'path'; import { defineConfig } from 'vite'; +import { configDefaults } from 'vitest/config'; dns.setDefaultResultOrder('verbatim'); @@ -43,6 +44,6 @@ export default defineConfig({ environment: 'happy-dom', // e2e tests need a real server + node WebSocket; run them via the // dedicated `pnpm test:e2e` config (vitest.e2e.config.mts), not the unit run. - exclude: ['**/node_modules/**', '**/*.e2e.test.ts'], + exclude: [...configDefaults.exclude, '**/*.e2e.test.ts'], }, });