From bfd4859383e5ee46097f3fd2e793301e1d6240ad Mon Sep 17 00:00:00 2001 From: ammann <161159040+agammann@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:50:05 -0700 Subject: [PATCH] Clean up failed native registrations and verify the containerized product --- .github/workflows/verify.yml | 16 +++-- README.md | 7 +- apps/reference-app/package.json | 2 +- apps/tally/package.json | 10 +-- docs/RELEASE-READINESS.md | 4 +- packages/shared/src/webmcp.ts | 7 +- pnpm-lock.yaml | 94 +++++++++++++-------------- scripts/native-check.ts | 8 ++- tests/webmcp.test.ts | 41 ++++++++++++ vitest.config.ts => vitest.config.mts | 0 10 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 tests/webmcp.test.ts rename vitest.config.ts => vitest.config.mts (100%) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 139dd81..cc7fc85 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -21,23 +21,27 @@ jobs: - run: pnpm db:generate && pnpm db:migrate && pnpm build - run: pnpm exec tsc --noEmit && pnpm test && pnpm audit --prod - run: pnpm exec playwright install --with-deps chromium - - name: Start private production services + - name: Start containerized dashboard and production reference run: | + docker compose build app + docker compose --profile app up -d --wait app mkdir -p work - pnpm --filter tally-dashboard start > work/dashboard-server.log 2>&1 & pnpm --filter tally-reference start > work/reference-server.log 2>&1 & + ready=false for attempt in $(seq 1 30); do - if curl --fail --silent http://localhost:3000/api/ready; then break; fi + if curl --fail --silent http://localhost:3001/ > /dev/null; then ready=true; break; fi sleep 1 done + test "$ready" = true - run: pnpm test:browser - run: pnpm test:integration && pnpm test:independent && pnpm test:operations - - run: pnpm exec tsx scripts/native-check.ts + - name: Require real native WebMCP checks + env: + TALLY_REQUIRE_NATIVE: '1' + run: pnpm exec tsx scripts/native-check.ts - run: pnpm exec tsx scripts/workflow-boundary.ts && pnpm exec tsx scripts/recovery-check.ts - run: pnpm exec tsx scripts/performance.ts - run: pnpm run licenses - - name: Build container - run: docker compose build app - uses: actions/upload-artifact@v4 with: name: tally-sdk-and-verification diff --git a/README.md b/README.md index 40dacdf..f41a693 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,13 @@ Manual tests, ordinary application controls and native callbacks have distinct a ## Verify -Use a **disposable local installation** for the verification scripts. The browser suite creates its own random QA owner credential in ignored `work/qa-state.json`. It must never run against an installation with a real owner. Run the dashboard and reference application first. +Use a **disposable local installation** for the verification scripts. The browser suite creates its own random QA owner credential in ignored `work/qa-state.json`. It must never run against an installation with a real owner. Complete installation and database migration above first. Generate the Prisma client and build the workspace packages before typechecking a fresh checkout; then run the dashboard and reference application for the browser and integration checks. ```sh +pnpm db:generate +pnpm build pnpm exec tsc --noEmit pnpm test -pnpm build pnpm exec playwright install chromium pnpm test:browser pnpm test:integration @@ -90,6 +91,8 @@ pnpm audit --prod The operations test restarts this Compose database and creates/removes only the named disposable `tally_restore_check` and `tally_upgrade_check` databases. The integration test creates verification projects, manipulates their limit counters, expires verification sessions, and tests the owner login limiter. Do not run these tests on a live installation. +CI sets `TALLY_REQUIRE_NATIVE=1` for the native check, so missing browser capabilities fail that job. An ordinary local run records unsupported native APIs as unverified. Native checks exercise the browser's tool discovery and execution APIs, unauthorized project rejection, visible filter changes, persisted outcomes, and removal of tools after sign-out. + ## Documentation - [SDK installation and API](docs/INTEGRATION.md) diff --git a/apps/reference-app/package.json b/apps/reference-app/package.json index cf3e9df..1dbbc9b 100644 --- a/apps/reference-app/package.json +++ b/apps/reference-app/package.json @@ -12,7 +12,7 @@ "dependencies": { "@tally-local/sdk": "workspace:*", "@tally-local/shared": "workspace:*", - "next": "16.3.4", + "next": "16.3.6", "react": "19.2.8", "react-dom": "19.2.8", "zod": "4.5.4" diff --git a/apps/tally/package.json b/apps/tally/package.json index eaa7a13..39bd82a 100644 --- a/apps/tally/package.json +++ b/apps/tally/package.json @@ -10,17 +10,17 @@ "start:container": "next start --hostname 0.0.0.0" }, "dependencies": { + "@prisma/client": "6.19.3", + "@tailwindcss/postcss": "4.3.3", "@tally-local/sdk": "workspace:*", "@tally-local/shared": "workspace:*", - "@prisma/client": "6.19.3", "better-auth": "1.7.3", - "next": "16.3.4", + "lucide-react": "^0.577.0", + "next": "16.3.6", "react": "19.2.8", "react-dom": "19.2.8", "recharts": "3.10.1", - "zod": "4.5.4", - "lucide-react": "^0.577.0", "tailwindcss": "4.3.3", - "@tailwindcss/postcss": "4.3.3" + "zod": "4.5.4" } } diff --git a/docs/RELEASE-READINESS.md b/docs/RELEASE-READINESS.md index c892945..4225c00 100644 --- a/docs/RELEASE-READINESS.md +++ b/docs/RELEASE-READINESS.md @@ -44,7 +44,7 @@ After the security correction, a separate boundary fixture calculated 25,000 wor 3. Native support is verified only for the documented flagged Chromium version. No claim covers unwrapped callbacks, declarative forms, rejection before callback entry, other browsers or automatic provider attribution. See [compatibility](COMPATIBILITY.md). 4. SDK 0.1.0 preserves ordinary synchronous values and native Promise outcomes. It does not treat custom thenables as promises. Promise object identity is not preserved. 5. One owner, up to 20 projects, one named workflow per project, immutable workflow definition and 30 minute observation window. New workflows remain open until the window matures, including those already completed early. -6. Remote HTTPS, secure host access, Windows file ACLs, backup encryption/expiration and retention scheduling are operator responsibilities. No public deployment or hosted service has been configured. The container favors a reproducible source build and includes build dependencies; its measured local image is approximately 4 GB. +6. Remote HTTPS, secure host access, Windows file ACLs, backup encryption/expiration and retention scheduling are operator responsibilities. These checks cover the self-hosted PostgreSQL edition. The hosted D1 service linked from the README is a separately maintained Sites deployment; this historical assessment does not certify it. The container favors a reproducible source build and includes build dependencies; its measured local image is approximately 4 GB. 7. Package namespace and product naming rights have not been established. `@tally-local/sdk` is a provisional local archive name. No npm publication or software license grant has been made. 8. [DEPENDENCIES.json](DEPENDENCIES.json) inspected 222 installed package manifest license fields. Flagged families include LGPL in a Sharp native package, MPL in Lightning CSS, CC BY in caniuse data and composite license expressions. These need distribution specific review and appropriate notices/source obligations before commercial redistribution. No legal clearance is claimed. 9. CI configuration is supplied. Local execution evidence above is distinct from a GitHub Actions run. Consult the repository Actions page for the result on a particular public commit. @@ -54,5 +54,3 @@ No essential feature is a placeholder. No external credential blocked the local ## Security report context The generated Codex Security report is delivered separately as `tally-security/report.md`, alongside sealed canonical JSON. It records the original unversioned snapshot. The workbench explicitly notes that source changed during the scan because the builder applied the verified corrections. It must not be described as an immutable audit of the later public commit. The recorded finding remains visible as historical evidence and its text states that it was fixed before publication. - -The scan tool reports 7,219,683 processed tokens across four task logs, including 6,763,392 cached input tokens. This is workbench accounting for the captured logs, not a separately measured security-only cost. diff --git a/packages/shared/src/webmcp.ts b/packages/shared/src/webmcp.ts index 5af35c5..fa6452a 100644 --- a/packages/shared/src/webmcp.ts +++ b/packages/shared/src/webmcp.ts @@ -5,8 +5,9 @@ export function nativeContext():Context|undefined {return typeof document==='und export async function registerTools(tools:BrowserTool[],signal:AbortSignal):Promise{ const ctx=nativeContext();if(!ctx?.registerTool)return 'Native WebMCP unavailable. Ordinary controls and labeled manual tests remain available.'; const registered:string[]=[]; - const cleanup=()=>{for(const name of registered){try{ctx.unregisterTool?.(name);}catch{ /* old implementation cleanup is best effort */ }}}; + const registration=new AbortController(); + const cleanup=()=>{registration.abort();signal.removeEventListener('abort',cleanup);for(const name of registered){try{ctx.unregisterTool?.(name);}catch{ /* old implementation cleanup is best effort */ }}}; signal.addEventListener('abort',cleanup,{once:true}); - try{for(const tool of tools){if(signal.aborted)break;await ctx.registerTool(tool,{signal});registered.push(tool.name);}if(signal.aborted){cleanup();return 'Native registration stopped.';}return 'Native WebMCP registered. Execution source and outcomes are application-reported.';} - catch {cleanup();return 'Native WebMCP registration failed. Use ordinary controls or labeled manual tests.';} + try{for(const tool of tools){if(signal.aborted)break;await ctx.registerTool(tool,{signal:registration.signal});registered.push(tool.name);}if(signal.aborted){cleanup();return 'Native registration stopped.';}return 'Native WebMCP registered. Execution source and outcomes are application-reported.';} + catch {cleanup();return signal.aborted?'Native registration stopped.':'Native WebMCP registration failed. Use ordinary controls or labeled manual tests.';} } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5021e0f..fd24878 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) better-auth: specifier: 1.7.3 - version: 1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))) + version: 1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))) zod: specifier: 4.5.4 version: 4.5.4 @@ -59,8 +59,8 @@ importers: specifier: workspace:* version: link:../../packages/shared next: - specifier: 16.3.4 - version: 16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.6 + version: 16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: 19.2.8 version: 19.2.8 @@ -87,13 +87,13 @@ importers: version: link:../../packages/shared better-auth: specifier: 1.7.3 - version: 1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))) + version: 1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))) lucide-react: specifier: ^0.577.0 version: 0.577.0(react@19.2.8) next: - specifier: 16.3.4 - version: 16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: 16.3.6 + version: 16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: specifier: 19.2.8 version: 19.2.8 @@ -550,57 +550,57 @@ packages: os: [linux] libc: [glibc] - '@next/env@16.3.4': - resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} - '@next/swc-darwin-arm64@16.3.4': - resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.4': - resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.4': - resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.4': - resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.4': - resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.4': - resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.4': - resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.4': - resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1579,8 +1579,8 @@ packages: resolution: {integrity: sha512-rQLB6eV4f2AW/n3L0JmwCROpaisYy9EDEADvEFSd1C/qG8hB6O5TPlh9A791JRbJr4CnMQBzptDcvD9OR1+6WA==} engines: {node: ^20.0.0 || >=22.0.0} - next@16.3.4: - resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -2267,30 +2267,30 @@ snapshots: '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true - '@next/env@16.3.4': {} + '@next/env@16.3.6': {} - '@next/swc-darwin-arm64@16.3.4': + '@next/swc-darwin-arm64@16.3.6': optional: true - '@next/swc-darwin-x64@16.3.4': + '@next/swc-darwin-x64@16.3.6': optional: true - '@next/swc-linux-arm64-gnu@16.3.4': + '@next/swc-linux-arm64-gnu@16.3.6': optional: true - '@next/swc-linux-arm64-musl@16.3.4': + '@next/swc-linux-arm64-musl@16.3.6': optional: true - '@next/swc-linux-x64-gnu@16.3.4': + '@next/swc-linux-x64-gnu@16.3.6': optional: true - '@next/swc-linux-x64-musl@16.3.4': + '@next/swc-linux-x64-musl@16.3.6': optional: true - '@next/swc-win32-arm64-msvc@16.3.4': + '@next/swc-win32-arm64-msvc@16.3.6': optional: true - '@next/swc-win32-x64-msvc@16.3.4': + '@next/swc-win32-x64-msvc@16.3.6': optional: true '@noble/ciphers@2.4.0': {} @@ -2647,7 +2647,7 @@ snapshots: baseline-browser-mapping@2.11.21: {} - better-auth@1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))): + better-auth@1.7.3(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(next@16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(prisma@6.19.3(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.11(@types/node@24.13.3)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13))): dependencies: '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3) '@better-auth/drizzle-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2) @@ -2668,7 +2668,7 @@ snapshots: zod: 4.5.4 optionalDependencies: '@prisma/client': 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) - next: 16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) prisma: 6.19.3(typescript@5.9.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -3020,9 +3020,9 @@ snapshots: nanostores@1.5.3: {} - next@16.3.4(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.6(@playwright/test@1.63.0)(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.3.4 + '@next/env': 16.3.6 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.21 caniuse-lite: 1.0.30001810 @@ -3031,14 +3031,14 @@ snapshots: react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.4 - '@next/swc-darwin-x64': 16.3.4 - '@next/swc-linux-arm64-gnu': 16.3.4 - '@next/swc-linux-arm64-musl': 16.3.4 - '@next/swc-linux-x64-gnu': 16.3.4 - '@next/swc-linux-x64-musl': 16.3.4 - '@next/swc-win32-arm64-msvc': 16.3.4 - '@next/swc-win32-x64-msvc': 16.3.4 + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 '@playwright/test': 1.63.0 sharp: 0.35.4(@types/node@24.13.3) transitivePeerDependencies: diff --git a/scripts/native-check.ts b/scripts/native-check.ts index 73c0aed..f5050c2 100644 --- a/scripts/native-check.ts +++ b/scripts/native-check.ts @@ -3,20 +3,24 @@ async function main(){const browser=await chromium.launch({headless:true,args:[' try{ await page.goto('http://localhost:3000/login');await page.getByLabel('Email',{exact:true}).fill(qa.email);await page.getByLabel('Password',{exact:true}).fill(qa.password);await page.getByRole('button',{name:'Sign in',exact:true}).click();await page.getByRole('heading',{name:'Overview',exact:true}).waitFor({timeout:30000}); const capabilities=await page.evaluate(()=>{const d=document as any,n=navigator as any;return {documentModelContext:!!d.modelContext,navigatorModelContext:!!n.modelContext,register:typeof d.modelContext?.registerTool,getTools:typeof d.modelContext?.getTools,executeTool:typeof d.modelContext?.executeTool,testing:!!n.modelContextTesting};});evidence.capabilities=capabilities; - if(capabilities.getTools!=='function'||capabilities.executeTool!=='function'){evidence.status='unverified: tested browser does not expose draft document.modelContext getTools/executeTool';console.log(JSON.stringify(evidence));return;} - await page.waitForFunction(()=>!!(document as any).modelContext); + if(capabilities.getTools!=='function'||capabilities.executeTool!=='function'){evidence.status='unverified: tested browser does not expose draft document.modelContext getTools/executeTool';console.log(JSON.stringify(evidence));if(process.env.TALLY_REQUIRE_NATIVE==='1')throw Error(evidence.status as string);return;} + await page.waitForFunction(async()=>{const tools=await (document as any).modelContext.getTools();return ['get_tool_metrics','get_tool_failures','set_dashboard_filters'].every(name=>tools.some((t:any)=>t.name===name));}); const from=new Date(Date.now()-86400000).toISOString(),to=new Date(Date.now()+10000).toISOString(); const dashboard=await page.evaluate(async f=>{const m=(document as any).modelContext;const ts=await m.getTools();const names=ts.map((t:any)=>t.name);const metrics=await m.executeTool(ts.find((t:any)=>t.name==='get_tool_metrics'),JSON.stringify(f));const failures=await m.executeTool(ts.find((t:any)=>t.name==='get_tool_failures'),JSON.stringify(f));await m.executeTool(ts.find((t:any)=>t.name==='set_dashboard_filters'),JSON.stringify({...f,source:'manual'}));return {names,metrics,failures};},{projectId:qa.referenceId!,from,to});evidence.dashboard=dashboard;assert.equal(await page.getByRole('combobox',{name:'Execution source',exact:true}).inputValue(),'manual'); + evidence.unauthorized=await page.evaluate(async f=>{const m=(document as any).modelContext;const tools=await m.getTools();try{await m.executeTool(tools.find((t:any)=>t.name==='get_tool_metrics'),JSON.stringify(f));return {rejected:false};}catch{return {rejected:true};}},{projectId:crypto.randomUUID(),from,to});assert.equal((evidence.unauthorized as {rejected:boolean}).rejected,true); await page.goto('http://localhost:3001');await page.getByLabel('Public ingestion identifier').fill(qa.referenceIngestionId!);await page.getByRole('button',{name:'Connect reference project'}).click();await page.getByRole('status').filter({hasText:'Reference project connected'}).waitFor(); const run=async(name:string,args:unknown)=>page.evaluate(async({name,args})=>{const m=(document as any).modelContext,tools=await m.getTools();try{return {ok:true,result:await m.executeTool(tools.find((t:any)=>t.name===name),JSON.stringify(args))};}catch(e){return {ok:false,name:e instanceof Error?e.name:'unknown'};}},{name,args}); evidence.success=await run('search_products',{query:''});await page.getByRole('combobox',{name:'Deterministic scenario'}).selectOption('failure');evidence.failure=await run('check_delivery',{zone:'local'}); + assert.equal((evidence.success as {ok:boolean}).ok,true);assert.equal((evidence.failure as {ok:boolean}).ok,false); await page.getByRole('combobox',{name:'Deterministic scenario'}).selectOption('cancellation');evidence.cancellation=await page.evaluate(async()=>{const m=(document as any).modelContext,tools=await m.getTools(),ac=new AbortController();const p=m.executeTool(tools.find((t:any)=>t.name==='check_delivery'),JSON.stringify({zone:'local'}),{signal:ac.signal});setTimeout(()=>ac.abort(),50);try{return {resolved:await p};}catch(e){return {rejected:true,name:e instanceof Error?e.name:'unknown'};}}); await page.getByRole('button',{name:'Flush telemetry'}).click();await page.getByRole('status').filter({hasText:'Flush finished.'}).waitFor(); + assert.equal((evidence.cancellation as {name:string}).name,'AbortError'); const db=new PrismaClient({log:[]});try{ const events=await db.event.findMany({where:{projectId:qa.referenceId!,source:'webmcp',receivedAt:{gte:new Date(evidence.checkedAt as string)}},select:{kind:true,outcome:true,tool:true,source:true}}); for(const outcome of ['success','failure','cancellation'])assert.ok(events.some(e=>e.kind==='tool_finished'&&e.outcome===outcome),'Persisted native outcome: '+outcome); evidence.persisted={events:events.length,terminalOutcomes:events.filter(e=>e.kind==='tool_finished').map(e=>({tool:e.tool,outcome:e.outcome,source:e.source}))}; }finally{await db.$disconnect();} + await page.goto('http://localhost:3000');await page.getByRole('heading',{name:'Overview',exact:true}).waitFor();await page.getByRole('button',{name:'Sign out',exact:true}).click();await page.getByRole('heading',{name:'Welcome back',exact:true}).waitFor();const afterSignOut=await page.evaluate(async()=>await (document as any).modelContext.getTools());assert.equal(afterSignOut.length,0);evidence.signedOutTools=0; evidence.status='verified: native draft APIs, visible filter change, and persisted webmcp success/failure/cancellation in flagged Chromium';console.log(JSON.stringify({browser:evidence.browser,capabilities:evidence.capabilities,persisted:evidence.persisted,status:evidence.status})); }finally{writeFileSync('work/native-results.json',JSON.stringify(evidence,null,2));await browser.close();}} main().catch(e=>{console.error(e);process.exitCode=1;}); diff --git a/tests/webmcp.test.ts b/tests/webmcp.test.ts new file mode 100644 index 0000000..54bf04a --- /dev/null +++ b/tests/webmcp.test.ts @@ -0,0 +1,41 @@ +import {afterEach, describe, expect, it, vi} from 'vitest'; +import {registerTools, type BrowserTool} from '../packages/shared/src/webmcp'; + +afterEach(() => vi.unstubAllGlobals()); +const tools:BrowserTool[] = ['first', 'second'].map(name => ({name, description:name, execute:() => name})); + +function browserContext(failSecond=false, legacy=false) { + const active=new Set(); + const unregisterTool=vi.fn((name:string) => active.delete(name)); + const registerTool=vi.fn(async(tool:BrowserTool, options?:{signal:AbortSignal}) => { + if(failSecond && tool.name==='second') throw Error('Registration rejected'); + active.add(tool.name); + options?.signal.addEventListener('abort', () => active.delete(tool.name), {once:true}); + }); + vi.stubGlobal('document', {modelContext:{registerTool,...(legacy?{unregisterTool}:{})}}); + return {active,registerTool,unregisterTool}; +} + +describe('native registration lifecycle', () => { + it('removes every earlier registration when a later registration fails without the legacy API', async() => { + const {active}=browserContext(true); + expect(await registerTools(tools,new AbortController().signal)).toContain('registration failed'); + expect([...active]).toEqual([]); + }); + it('links successful registrations to the caller lifetime', async() => { + const {active}=browserContext();const lifetime=new AbortController(); + expect(await registerTools(tools,lifetime.signal)).toContain('registered'); + expect([...active]).toEqual(['first','second']);lifetime.abort(); + expect([...active]).toEqual([]); + }); + it('retains cleanup for older implementations', async() => { + const {unregisterTool}=browserContext(true,true); + await registerTools(tools,new AbortController().signal); + expect(unregisterTool).toHaveBeenCalledExactlyOnceWith('first'); + }); + it('does not register tools for an already ended lifetime', async() => { + const {registerTool}=browserContext();const lifetime=new AbortController();lifetime.abort(); + expect(await registerTools(tools,lifetime.signal)).toContain('stopped'); + expect(registerTool).not.toHaveBeenCalled(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.mts similarity index 100% rename from vitest.config.ts rename to vitest.config.mts