diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml new file mode 100644 index 0000000..64c7a84 --- /dev/null +++ b/.github/workflows/pr-quality.yml @@ -0,0 +1,37 @@ +name: PR Quality + +on: + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-build-coverage: + name: Test, coverage e build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Install + run: npm ci + + - name: Test + run: npm test + + - name: Coverage core + run: npm run test:coverage + + - name: Build + run: npm run build diff --git a/README.md b/README.md index 681365a..b41b4a7 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Monitor configurati: npm install npm run build npm test +npm run test:coverage npm run sentinel -- scan npm run sentinel -- scan --dry-run npm run sentinel -- report @@ -113,6 +114,17 @@ Su macOS, se la password non è in env, Sentinel prova a leggere dal Portachiavi - [docs/TOOLCHAIN.md](docs/TOOLCHAIN.md): runtime, workflow e verifiche. - [docs/DECISIONS.md](docs/DECISIONS.md): decisioni stabili. +## Coverage core + +Per l'audit Atlas la coverage Vitest ufficiale passa da: + +```bash +npm run test:coverage +``` + +Il perimetro core attuale è dichiarato in `vitest.config.ts` e applica soglie +minime `75%` linee e `65%` branch sui moduli core selezionati. + ## Pubblicazione La pubblicazione codice passa da GitHub. Il deploy operativo MVP passa dal diff --git a/docs/TOOLCHAIN.md b/docs/TOOLCHAIN.md index 53af36a..19256d3 100644 --- a/docs/TOOLCHAIN.md +++ b/docs/TOOLCHAIN.md @@ -36,6 +36,7 @@ Questa pagina descrive runtime, comandi e guardrail effettivi di Sentinel. | Vercel CLI | locale/autenticata | deploy dashboard web senza GitHub Actions | | Vercel Blob | privato | payload dinamico dashboard e ultimi report | | GitHub Actions | `ubuntu-latest`, Node `24` | runtime operativo schedulato/manuale | +| GitHub Actions PR Quality | `ubuntu-latest`, Node `24` | gate PR su test, coverage core e build | | Dependabot | configurazione GitHub | aggiornamenti dipendenze npm e GitHub Actions | | SMTP Gmail | Doppler, secret GitHub o env locale | invio email operativo | | Portachiavi macOS | servizi `sentinel-gmail` e `sentinel-icloud` | fallback locale per password email | @@ -45,6 +46,8 @@ Questa pagina descrive runtime, comandi e guardrail effettivi di Sentinel. - install/setup: `npm install` o `npm ci`. - build: `npm run build`. - test: `npm test`. +- coverage core Atlas: `npm run test:coverage`. +- gate PR quality: workflow `.github/workflows/pr-quality.yml`. - Codex comments dry-run: workflow `Codex PR comments` con input `dry_run=true`. - scan: `npm run sentinel -- scan`. - dry-run scan: `npm run sentinel -- scan --dry-run`. @@ -59,6 +62,14 @@ Questa pagina descrive runtime, comandi e guardrail effettivi di Sentinel. - test email Gmail: `npm run sentinel -- test-email --profile gmail`. - test email iCloud: `npm run sentinel -- test-email --profile icloud`. +## Coverage core + +- La coverage Vitest ufficiale per l'audit Atlas gira con `npm run test:coverage`. +- Il perimetro core corrente è dichiarato in `vitest.config.ts`. +- Le soglie minime correnti sul perimetro core sono `75%` linee e `65%` branch. +- La coverage non sostituisce `npm test` e `npm run build`: è un gate aggiuntivo + quando il lavoro tocca test, quality bar o moduli core. + ## Verifiche per scope | Tipo modifica | Corsia | Verifiche minime | @@ -67,6 +78,7 @@ Questa pagina descrive runtime, comandi e guardrail effettivi di Sentinel. | Docs-only | veloce | Review documentale e `git diff --check` quando utile | | Workflow/config o documenti operativi critici | standard | Review mirata e comando collegato al file modificato | | Test-only, CLI o dashboard piccola | standard | `npm test`, `npm run build` o test mirati | +| Audit test/coverage o quality bar moduli core | standard | `npm test`, `npm run test:coverage`, `npm run build` | | Runtime schedulato, dati/output, provider email, deploy/config, release/versioning o UI sostanziale | completa | Gate completo proporzionato, smoke/manual run quando serve, React Doctor se applicabile | Per UI sostanziale della dashboard Next.js usare anche browser locale o deploy diff --git a/package-lock.json b/package-lock.json index b593bc7..3c5a651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/nodemailer": "^8.0.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^4.1.7", "tsx": "^4.20.6", "typescript": "^6.0.3", "vitest": "^4.0.14" @@ -36,6 +37,66 @@ "npm": ">=11 <12" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -994,6 +1055,16 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1001,6 +1072,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -1167,9 +1249,9 @@ "license": "MIT" }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -1177,9 +1259,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1194,9 +1276,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1211,9 +1293,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1228,9 +1310,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1245,9 +1327,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1262,13 +1344,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1279,13 +1364,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1296,13 +1384,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1313,13 +1404,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1330,13 +1424,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1347,13 +1444,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1364,9 +1464,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -1381,9 +1481,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -1400,9 +1500,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -1417,9 +1517,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1557,17 +1657,48 @@ "node": ">=18.17" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "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.8", + "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1576,13 +1707,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.7", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1603,9 +1734,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -1616,13 +1747,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.7", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -1630,14 +1761,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1646,9 +1777,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -1656,13 +1787,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", + "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1680,6 +1811,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -2069,6 +2212,23 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -2129,6 +2289,52 @@ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "license": "MIT" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2272,6 +2478,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2293,6 +2502,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2314,6 +2526,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2335,6 +2550,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2400,6 +2618,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -2661,13 +2907,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2677,21 +2923,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/scheduler": { @@ -2704,8 +2950,8 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -2823,6 +3069,19 @@ } } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/throttleit": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", @@ -2853,9 +3112,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2935,17 +3194,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -3013,19 +3272,19 @@ } }, "node_modules/vitest": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.7", - "@vitest/mocker": "4.1.7", - "@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.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -3053,12 +3312,12 @@ "@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.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 5366b5c..5a55c7d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dev:cli": "tsx src/index.ts", "sentinel": "tsx src/index.ts", "test": "vitest run test", + "test:coverage": "vitest run test --coverage", "doctor": "npx --yes react-doctor@latest --full --fail-on error .", "quality:react-doctor": "npx --yes react-doctor@latest --full --fail-on error ." }, @@ -39,6 +40,7 @@ "@types/nodemailer": "^8.0.0", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^4.1.7", "tsx": "^4.20.6", "typescript": "^6.0.3", "vitest": "^4.0.14" diff --git a/test/fetch-resource.test.ts b/test/fetch-resource.test.ts index fc717f8..680fdad 100644 --- a/test/fetch-resource.test.ts +++ b/test/fetch-resource.test.ts @@ -38,4 +38,70 @@ describe("fetchResource", () => { expect(resource.normalizedText).toBeUndefined(); expect(resource.hash).toHaveLength(64); }); + + it("estrae testo e URL scoperti da una pagina HTML nello stesso dominio", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response( + ` + + Test + + Catalogo + Fuori dominio + + + + `, + { + headers: { "content-type": "text/html; charset=utf-8" }, + status: 200 + } + )) + ); + + const resource = await fetchResource("https://example.com/prodotti", site, 1, "https://example.com/"); + + expect(resource.kind).toBe("html"); + expect(resource.title).toBe("Test"); + expect(resource.normalizedText).toContain("Catalogo"); + expect(resource.discoveredUrls).toEqual([ + "https://example.com/catalogo", + "https://example.com/media/hero.jpg" + ]); + }); + + it("restituisce una risorsa non OK senza hash quando l'URL resta HTML", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => new Response("not found", { + headers: { "content-type": "text/html" }, + status: 404 + })) + ); + + const resource = await fetchResource("https://example.com/mancante", site, 0); + + expect(resource.kind).toBe("html"); + expect(resource.status).toBe(404); + expect(resource.hash).toBe(""); + expect(resource.discoveredUrls).toEqual([]); + }); + + it("fallisce quando un redirect porta fuori dominio", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + url: "https://evil.example.net/landing", + ok: true, + status: 200, + headers: new Headers({ "content-type": "text/html" }), + text: async () => "evil" + })) + ); + + await expect(fetchResource("https://example.com/login", site, 0)).rejects.toThrow( + "Redirect fuori dominio: https://example.com/login -> https://evil.example.net/landing" + ); + }); }); diff --git a/test/fs.test.ts b/test/fs.test.ts new file mode 100644 index 0000000..67d7b83 --- /dev/null +++ b/test/fs.test.ts @@ -0,0 +1,50 @@ +import { readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { readJsonFile, resolveFromCwd, writeJsonFile } from "../src/fs.js"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function createTempDir(prefix: string): Promise { + const dir = await import("node:fs/promises").then(({ mkdtemp }) => mkdtemp(path.join(os.tmpdir(), prefix))); + tempDirs.push(dir); + return dir; +} + +describe("fs helpers", () => { + it("risolve i path relativi dalla working directory e lascia invariati quelli assoluti", () => { + const absolutePath = "/tmp/sentinel.json"; + + expect(resolveFromCwd("reports/latest.md")).toBe(path.resolve(process.cwd(), "reports/latest.md")); + expect(resolveFromCwd(absolutePath)).toBe(absolutePath); + }); + + it("scrive JSON creando le directory mancanti e lo rilegge correttamente", async () => { + const rootDir = await createTempDir("sentinel-fs-"); + const filePath = path.join(rootDir, "nested", "state.json"); + const payload = { + version: 1, + site: "ortix" + }; + + await writeJsonFile(filePath, payload); + + expect(await readFile(filePath, "utf8")).toBe('{\n "version": 1,\n "site": "ortix"\n}\n'); + await expect(readJsonFile(filePath, null)).resolves.toEqual(payload); + }); + + it("restituisce il fallback su file mancante e rilancia JSON non valido", async () => { + const rootDir = await createTempDir("sentinel-fs-invalid-"); + const missingFilePath = path.join(rootDir, "missing.json"); + const invalidFilePath = path.join(rootDir, "broken.json"); + await writeFile(invalidFilePath, "{invalid}\n", "utf8"); + + await expect(readJsonFile(missingFilePath, { empty: true })).resolves.toEqual({ empty: true }); + await expect(readJsonFile(invalidFilePath, null)).rejects.toThrow(); + }); +}); diff --git a/test/report.test.ts b/test/report.test.ts index ebbd17d..9bf8a3b 100644 --- a/test/report.test.ts +++ b/test/report.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { renderScanReport } from "../src/report.js"; -import type { ScanResult } from "../src/types.js"; +import { renderInventoryReport, renderScanReport } from "../src/report.js"; +import type { ScanResult, SentinelState } from "../src/types.js"; function baseResult(overrides: Partial): ScanResult { return { @@ -82,4 +82,67 @@ describe("scan report", () => { expect(report).toContain("## Avvisi noti"); expect(report).toContain("- Noto: https://example.com/legacy.jpg - HTTP 404 - Asset legacy"); }); + + it("aggiunge la sintesi baseline e rende tutti i tipi di cambiamento", () => { + const report = renderScanReport( + baseResult({ + baseline: true, + changes: [ + { type: "added", url: "https://example.com/a", title: "Nuova pagina" }, + { type: "changed", url: "https://example.com/b" }, + { type: "removed", url: "https://example.com/c" } + ] + }) + ); + + expect(report).toContain("Baseline iniziale creata. Nessuna email inviata per policy."); + expect(report).toContain("- Aggiunto: https://example.com/a - Nuova pagina"); + expect(report).toContain("- Modificato: https://example.com/b"); + expect(report).toContain("- Rimosso: https://example.com/c"); + }); + + it("riporta lo stato stabile quando non ci sono cambiamenti o problemi", () => { + const report = renderScanReport(baseResult({})); + + expect(report).toContain("Nessun cambiamento o problema attivo rilevato."); + expect(report).toContain("Nessun problema rilevato."); + expect(report).toContain("Nessun avviso noto rilevato."); + }); + + it("genera un inventario ordinato e mostra \"mai\" senza scansioni precedenti", () => { + const state: SentinelState = { + version: 1, + sites: { + ortix: { + id: "ortix", + name: "Ortix", + urls: { + b: { + url: "https://example.com/b", + kind: "file", + firstSeenAt: "2026-05-24T00:00:00.000Z", + lastSeenAt: "2026-05-24T00:00:00.000Z", + lastStatus: 200, + hash: "b", + snapshotIds: [] + }, + a: { + url: "https://example.com/a", + kind: "html", + firstSeenAt: "2026-05-24T00:00:00.000Z", + lastSeenAt: "2026-05-24T00:00:00.000Z", + lastStatus: 200, + hash: "a", + snapshotIds: [] + } + } + } + } + }; + + const report = renderInventoryReport(state, "2026-06-02T08:30:00.000Z"); + + expect(report).toContain("- Ultima scansione: mai"); + expect(report.indexOf("- HTML https://example.com/a")).toBeLessThan(report.indexOf("- FILE https://example.com/b")); + }); }); diff --git a/test/storage.test.ts b/test/storage.test.ts new file mode 100644 index 0000000..953c5d3 --- /dev/null +++ b/test/storage.test.ts @@ -0,0 +1,207 @@ +import { readFile, rm, stat, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { urlId } from "../src/hash.js"; +import { + getOrCreateSiteState, + isInitialBaseline, + loadState, + pruneSnapshots, + saveState, + statePath, + toUrlState, + writeSnapshot +} from "../src/storage.js"; +import type { FetchedResource, SentinelConfig, SentinelState, SiteConfig } from "../src/types.js"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function createTempDir(prefix: string): Promise { + const dir = await import("node:fs/promises").then(({ mkdtemp }) => mkdtemp(path.join(os.tmpdir(), prefix))); + tempDirs.push(dir); + return dir; +} + +function createConfig(rootDir: string): SentinelConfig { + return { + version: 1, + storage: { + dataDir: path.join(rootDir, "data"), + snapshotsDir: path.join(rootDir, "snapshots"), + reportsDir: path.join(rootDir, "reports") + }, + email: { + enabled: true, + defaultProfile: "gmail", + fromEnv: "SENTINEL_EMAIL_FROM", + toEnv: "SENTINEL_EMAIL_TO", + subjectPrefix: "[Sentinel]", + profiles: {} + }, + sites: [createSite()] + }; +} + +function createSite(): SiteConfig { + return { + id: "ortix", + name: "Ortix", + enabled: true, + sitemapUrls: ["https://www.ortix.it/sitemap_index.xml"], + roots: ["https://www.ortix.it/"], + crawl: { + maxDepth: 3, + maxUrls: 500, + timeoutMs: 30000, + userAgent: "Sentinel test" + }, + includeFileExtensions: ["pdf"], + trackingParams: [], + ignoredIssues: [] + }; +} + +function createResource(overrides: Partial = {}): FetchedResource { + return { + url: "https://www.ortix.it/catalogo", + finalUrl: "https://www.ortix.it/catalogo", + kind: "html", + status: 200, + contentType: "text/html; charset=utf-8", + contentLength: 321, + etag: "\"abc\"", + lastModified: "Mon, 01 Jun 2026 10:00:00 GMT", + hash: "deadbeef", + title: "Catalogo Ortix", + normalizedText: "catalogo aggiornato", + links: [], + fetchedAt: "2026-06-02T10:11:12.000Z", + ...overrides + }; +} + +describe("storage", () => { + it("salva e ricarica lo stato dal percorso configurato", async () => { + const rootDir = await createTempDir("sentinel-storage-"); + const config = createConfig(rootDir); + const state: SentinelState = { + version: 1, + sites: { + ortix: { + id: "ortix", + name: "Ortix", + lastScanAt: "2026-06-02T10:11:12.000Z", + urls: {} + } + } + }; + + await saveState(config, state); + + expect(statePath(config)).toBe(path.join(rootDir, "data", "state.json")); + await expect(loadState(config)).resolves.toEqual(state); + }); + + it("restituisce uno stato vuoto quando il file non esiste", async () => { + const rootDir = await createTempDir("sentinel-storage-missing-"); + const config = createConfig(rootDir); + + await expect(loadState(config)).resolves.toEqual({ + version: 1, + sites: {} + }); + }); + + it("inizializza il site state solo alla prima richiesta e rileva la baseline", () => { + const site = createSite(); + const state: SentinelState = { + version: 1, + sites: {} + }; + + const initialSiteState = getOrCreateSiteState(state, site); + + expect(initialSiteState).toEqual({ + id: "ortix", + name: "Ortix", + urls: {} + }); + expect(isInitialBaseline(initialSiteState)).toBe(true); + + initialSiteState.urls.home = { + url: "https://www.ortix.it/", + kind: "html", + firstSeenAt: "2026-06-02T10:00:00.000Z", + lastSeenAt: "2026-06-02T10:11:12.000Z", + lastStatus: 200, + hash: "abc", + snapshotIds: [] + }; + + expect(getOrCreateSiteState(state, site)).toBe(initialSiteState); + expect(isInitialBaseline(initialSiteState)).toBe(false); + }); + + it("scrive snapshot JSON e costruisce lo stato URL dal resource fetchato", async () => { + const rootDir = await createTempDir("sentinel-storage-snapshot-"); + const config = createConfig(rootDir); + const site = createSite(); + const resource = createResource(); + + const snapshotId = await writeSnapshot(config, site, resource); + const snapshotPath = path.join(rootDir, "snapshots", snapshotId); + const snapshot = JSON.parse(await readFile(snapshotPath, "utf8")) as Record; + + expect(snapshotId).toBe(`ortix/${urlId(resource.url)}/20260602T101112Z.json`); + expect(snapshot).toMatchObject({ + id: snapshotId, + capturedAt: resource.fetchedAt, + url: resource.url, + hash: resource.hash, + normalizedText: resource.normalizedText + }); + + expect(toUrlState(resource, "2026-06-01T09:00:00.000Z", [snapshotId])).toMatchObject({ + url: resource.url, + kind: "html", + firstSeenAt: "2026-06-01T09:00:00.000Z", + lastSeenAt: resource.fetchedAt, + lastStatus: 200, + hash: resource.hash, + snapshotIds: [snapshotId] + }); + }); + + it("mantiene solo gli ultimi snapshot e rimuove i precedenti dal filesystem", async () => { + const rootDir = await createTempDir("sentinel-storage-prune-"); + const config = createConfig(rootDir); + const snapshotIds = [ + "ortix/a/20260602T100000Z.json", + "ortix/a/20260602T101000Z.json", + "ortix/a/20260602T102000Z.json", + "ortix/a/20260602T103000Z.json" + ]; + + await Promise.all( + snapshotIds.map(async (snapshotId) => { + const snapshotPath = path.join(rootDir, "snapshots", snapshotId); + await import("node:fs/promises").then(({ mkdir }) => mkdir(path.dirname(snapshotPath), { recursive: true })); + await writeFile(snapshotPath, "{}\n", "utf8"); + }) + ); + + await expect(pruneSnapshots(config, snapshotIds, 3)).resolves.toEqual([ + "ortix/a/20260602T101000Z.json", + "ortix/a/20260602T102000Z.json", + "ortix/a/20260602T103000Z.json" + ]); + + await expect(stat(path.join(rootDir, "snapshots", snapshotIds[0]))).rejects.toThrow(); + await expect(stat(path.join(rootDir, "snapshots", snapshotIds[3]))).resolves.toBeDefined(); + }); +}); diff --git a/test/summary.test.ts b/test/summary.test.ts index 8edfc2c..eb54b63 100644 --- a/test/summary.test.ts +++ b/test/summary.test.ts @@ -38,4 +38,25 @@ describe("scan summary", () => { expect(lines[2]).toBe("- Avviso https://example.com/a: HTTP 404"); expect(lines[3]).toBe("- Avvisi noti ignorati dal conteggio problemi: 1"); }); + + it("riporta baseline, percorso report e problemi omessi oltre la soglia visibile", () => { + const lines = formatScanSummary( + baseResult({ + baseline: true, + reportPath: "/tmp/reports/ortix.md", + issues: [ + { url: "email", message: "Password SMTP assente", fatal: true }, + ...Array.from({ length: 12 }, (_, index) => ({ + url: `https://example.com/${index}`, + message: "HTTP 404", + fatal: false + })) + ] + }) + ); + + expect(lines[1]).toBe("Baseline iniziale: email non inviata."); + expect(lines[2]).toBe("Report: /tmp/reports/ortix.md"); + expect(lines.at(-1)).toBe("- Altri problemi: 3"); + }); }); diff --git a/test/text.test.ts b/test/text.test.ts index a6d8c89..95f41dd 100644 --- a/test/text.test.ts +++ b/test/text.test.ts @@ -28,4 +28,19 @@ describe("HTML text extraction", () => { ) ).toBe("Testo OUR INSTAGRAM Recent Comments"); }); + + it("estrae anche asset da srcset e gestisce pagine senza title", () => { + const result = extractNormalizedText(` +
+ Testo semplice + + +
+ `); + + expect(result.title).toBeUndefined(); + expect(result.text).toBe("Testo semplice"); + expect(result.links).toEqual([]); + expect(result.assets).toEqual(["/hero-small.jpg", "/hero-large.jpg", "/video.mp4"]); + }); }); diff --git a/test/time.test.ts b/test/time.test.ts new file mode 100644 index 0000000..a3261d9 --- /dev/null +++ b/test/time.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { formatItalianDateTime, timestampForFile } from "../src/time.js"; + +describe("time helpers", () => { + it("compatta i timestamp in formato file-safe", () => { + expect(timestampForFile(new Date("2026-06-02T10:11:12.345Z"))).toBe("20260602T101112Z"); + }); + + it("formatta la data nel fuso Europe/Rome", () => { + const formatted = formatItalianDateTime("2026-06-02T08:30:00.000Z"); + + expect(formatted).toMatch(/10:30/); + expect(formatted).not.toMatch(/08:30/); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c61c532 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + provider: "v8", + include: [ + "src/config.ts", + "src/dashboard.ts", + "src/fetch-resource.ts", + "src/fs.ts", + "src/report.ts", + "src/storage.ts", + "src/summary.ts", + "src/text.ts", + "src/time.ts", + "src/url.ts" + ], + reporter: ["text", "json-summary"], + thresholds: { + lines: 75, + branches: 65 + } + } + } +});