diff --git a/.gitignore b/.gitignore index dc5f8d103..88f9449e4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ *.log test/screenshots/*.png test/artifacts/ +*.gesture-telemetry.json .build/ .swiftpm/ DerivedData/ diff --git a/package.json b/package.json index 94153a283..66dcfcd21 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "prepublishOnly": "pnpm build:all", "prepack": "pnpm build:all", "typecheck": "tsc -p tsconfig.json", - "test": "node --test", - "test:unit": "node --test src/__tests__/*.test.ts src/core/__tests__/*.test.ts src/daemon/__tests__/*.test.ts src/daemon/handlers/__tests__/*.test.ts src/platforms/**/__tests__/*.test.ts src/utils/**/__tests__/*.test.ts", + "test": "node --test && vitest run", + "test:unit": "node --test src/__tests__/*.test.ts src/core/__tests__/*.test.ts src/daemon/__tests__/*.test.ts src/daemon/handlers/__tests__/*.test.ts src/platforms/**/__tests__/*.test.ts src/utils/**/__tests__/*.test.ts && vitest run", "test:smoke": "node --test test/integration/smoke-*.test.ts", "test:integration": "node --test test/integration/*.test.ts" }, @@ -78,6 +78,7 @@ "@types/node": "^22.0.0", "@types/pngjs": "^6.0.5", "prettier": "^3.3.3", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbdc5a349..199c712ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.2(@types/node@22.19.7)(vite@8.0.3(@types/node@22.19.7)(jiti@2.6.1)) website: devDependencies: @@ -130,6 +133,9 @@ packages: '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -176,6 +182,101 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@rsbuild/core@1.7.2': resolution: {integrity: sha512-VAFO6cM+cyg2ntxNW6g3tB2Jc5J5mpLjLluvm7VtW2uceNzyUlVv41o66Yp1t1ikxd3ljtqegViXem62JqzveA==} engines: {node: '>=18.12.0'} @@ -428,6 +529,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -440,9 +544,15 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -510,6 +620,35 @@ packages: vue-router: optional: true + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -546,6 +685,10 @@ packages: argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -579,6 +722,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -608,6 +755,9 @@ packages: compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -637,6 +787,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -651,6 +805,9 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -687,6 +844,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -871,6 +1032,76 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} @@ -884,6 +1115,9 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -1083,6 +1317,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1090,6 +1329,9 @@ packages: nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -1105,6 +1347,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1120,6 +1365,10 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} @@ -1252,6 +1501,11 @@ packages: engines: {node: '>= 0.4'} hasBin: true + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rsbuild-plugin-dts@0.19.4: resolution: {integrity: sha512-IDJsg3m9ZT/Jm5RU2e8Hits8b3vI2OWdCKxkly5uDB2owXGWAaswEy3H6uTR9GhnJlQGzvfOTxwy3JQeltiLqQ==} engines: {node: '>=18.12.0'} @@ -1298,6 +1552,13 @@ packages: resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} engines: {node: '>=20'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -1312,9 +1573,15 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -1344,6 +1611,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -1352,6 +1626,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1424,9 +1702,92 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + 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.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -1534,6 +1895,8 @@ snapshots: tslib: 2.8.1 optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -1645,6 +2008,57 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@oxc-project/types@0.122.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@rsbuild/core@1.7.2': dependencies: '@rspack/core': 1.7.4(@swc/helpers@0.5.18) @@ -1953,6 +2367,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -1968,10 +2384,17 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -2017,6 +2440,47 @@ snapshots: optionalDependencies: react: 19.2.4 + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@8.0.3(@types/node@22.19.7)(jiti@2.6.1))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.3(@types/node@22.19.7)(jiti@2.6.1) + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2047,6 +2511,8 @@ snapshots: dependencies: sprintf-js: 1.0.3 + assertion-error@2.0.1: {} + astring@1.9.0: {} bail@2.0.2: {} @@ -2069,6 +2535,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -2097,6 +2565,8 @@ snapshots: compute-scroll-into-view@3.1.1: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} copy-to-clipboard@3.3.3: @@ -2117,6 +2587,8 @@ snapshots: dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -2129,6 +2601,8 @@ snapshots: dependencies: stackframe: 1.3.4 + es-module-lexer@2.0.0: {} + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -2180,6 +2654,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + expect-type@1.3.0: {} + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -2416,6 +2892,55 @@ snapshots: kind-of@6.0.3: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lodash-es@4.17.23: {} lodash@4.17.23: {} @@ -2426,6 +2951,10 @@ snapshots: dependencies: yallist: 4.0.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} @@ -2897,10 +3426,14 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.11: {} + normalize-path@3.0.0: {} nprogress@0.2.0: {} + obug@2.1.1: {} + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.5: @@ -2925,6 +3458,8 @@ snapshots: path-parse@1.0.7: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -2933,6 +3468,12 @@ snapshots: pngjs@7.0.0: {} + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@3.8.1: {} property-information@7.1.0: {} @@ -3107,6 +3648,27 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + rolldown@1.0.0-rc.12: + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + rsbuild-plugin-dts@0.19.4(@microsoft/api-extractor@7.57.7(@types/node@22.19.7))(@rsbuild/core@1.7.2)(typescript@5.9.3): dependencies: '@ast-grep/napi': 0.37.0 @@ -3147,6 +3709,10 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + source-map@0.6.1: {} source-map@0.7.6: {} @@ -3155,8 +3721,12 @@ snapshots: sprintf-js@1.0.3: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} + std-env@4.0.0: {} + string-argv@0.3.2: {} stringify-entities@4.0.4: @@ -3182,6 +3752,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tinybench@2.9.0: {} + + tinyexec@1.0.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -3189,6 +3763,8 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3275,8 +3851,52 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + vite@8.0.3(@types/node@22.19.7)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.7 + fsevents: 2.3.3 + jiti: 2.6.1 + + vitest@4.1.2(@types/node@22.19.7)(vite@8.0.3(@types/node@22.19.7)(jiti@2.6.1)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.3(@types/node@22.19.7)(jiti@2.6.1)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.3(@types/node@22.19.7)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.7 + transitivePeerDependencies: + - msw + web-namespaces@2.0.1: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + yallist@4.0.0: {} zod@3.25.76: {} diff --git a/src/daemon/__tests__/request-router-android-modal.test.ts b/src/daemon/__tests__/request-router-android-modal.vitest.ts similarity index 68% rename from src/daemon/__tests__/request-router-android-modal.test.ts rename to src/daemon/__tests__/request-router-android-modal.vitest.ts index 98c56fe21..2350a5875 100644 --- a/src/daemon/__tests__/request-router-android-modal.test.ts +++ b/src/daemon/__tests__/request-router-android-modal.vitest.ts @@ -1,5 +1,4 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; +import { test, expect, vi } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -8,6 +7,48 @@ import { SessionStore } from '../session-store.ts'; import type { SessionState } from '../types.ts'; import { LeaseRegistry } from '../lease-registry.ts'; +let snapshotCalls = 0; + +vi.mock('../../platforms/android/index.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + snapshotAndroid: vi.fn(async () => { + snapshotCalls += 1; + if (snapshotCalls === 1) { + return { + nodes: [ + { + index: 0, + type: 'android.widget.TextView', + label: 'Process system is not responding', + rect: { x: 50, y: 400, width: 500, height: 80 }, + }, + { + index: 1, + type: 'android.widget.Button', + label: 'Close app', + rect: { x: 100, y: 600, width: 220, height: 80 }, + }, + ], + }; + } + return { nodes: [] }; + }), + openAndroidApp: vi.fn(async () => {}), + getAndroidAppState: vi.fn(async () => ({ package: 'com.android.settings' })), + }; +}); + +const execCalls: string[][] = []; + +vi.mock('../../utils/exec.ts', () => ({ + runCmd: vi.fn(async (_cmd: string, args: string[]) => { + execCalls.push(args); + return { stdout: '', stderr: '', exitCode: 0 }; + }), +})); + function makeStore(): SessionStore { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-router-android-modal-')); return new SessionStore(path.join(tempRoot, 'sessions')); @@ -40,12 +81,14 @@ function makeAndroidSession(name: string): SessionState { } test('generic Android gesture commands dismiss blocking system dialogs during recording', async () => { + snapshotCalls = 0; + execCalls.length = 0; + const sessionStore = makeStore(); sessionStore.set('default', makeAndroidSession('default')); const dispatchCalls: string[][] = []; - const execCalls: string[][] = []; - const reopenedApps: string[] = []; - let snapshotCalls = 0; + + const { openAndroidApp } = await import('../../platforms/android/index.ts'); const handler = createRequestHandler({ logPath: path.join(os.tmpdir(), 'daemon.log'), @@ -57,36 +100,6 @@ test('generic Android gesture commands dismiss blocking system dialogs during re dispatchCalls.push([command, ...positionals]); return {}; }, - snapshotAndroidUi: async () => { - snapshotCalls += 1; - if (snapshotCalls === 1) { - return { - nodes: [ - { - index: 0, - type: 'android.widget.TextView', - label: 'Process system is not responding', - rect: { x: 50, y: 400, width: 500, height: 80 }, - }, - { - index: 1, - type: 'android.widget.Button', - label: 'Close app', - rect: { x: 100, y: 600, width: 220, height: 80 }, - }, - ], - }; - } - return { nodes: [] }; - }, - reopenAndroidApp: async (_device, app) => { - reopenedApps.push(app); - }, - readAndroidAppState: async () => ({ package: 'com.android.settings' }), - execCommand: async (_cmd, args) => { - execCalls.push(args); - return { stdout: '', stderr: '', exitCode: 0 }; - }, }); const response = await handler({ @@ -97,9 +110,12 @@ test('generic Android gesture commands dismiss blocking system dialogs during re meta: { requestId: 'req-android-modal' }, }); - assert.equal(response.ok, true); - assert.deepEqual(dispatchCalls, [['scroll', 'down', '0.55']]); - assert.deepEqual(execCalls, [['-s', 'emulator-5554', 'shell', 'input', 'tap', '210', '640']]); - assert.deepEqual(reopenedApps, ['com.android.settings']); - assert.equal(snapshotCalls, 2); + expect(response.ok).toBe(true); + expect(dispatchCalls).toEqual([['scroll', 'down', '0.55']]); + expect(execCalls).toEqual([['-s', 'emulator-5554', 'shell', 'input', 'tap', '210', '640']]); + expect(openAndroidApp).toHaveBeenCalledWith( + expect.objectContaining({ id: 'emulator-5554' }), + 'com.android.settings', + ); + expect(snapshotCalls).toBe(2); }); diff --git a/src/daemon/android-system-dialog.ts b/src/daemon/android-system-dialog.ts index 145ecad7d..f6e798cd3 100644 --- a/src/daemon/android-system-dialog.ts +++ b/src/daemon/android-system-dialog.ts @@ -15,32 +15,22 @@ export type AndroidBlockingDialogRecoveryResult = 'absent' | 'recovered' | 'fail export async function recoverAndroidBlockingSystemDialog(params: { session: SessionState; - snapshotAndroidUi?: typeof snapshotAndroid; - reopenAndroidApp?: typeof openAndroidApp; - readAndroidAppState?: typeof getAndroidAppState; - execCommand?: typeof runCmd; }): Promise { - const { - session, - snapshotAndroidUi = snapshotAndroid, - reopenAndroidApp = openAndroidApp, - readAndroidAppState = getAndroidAppState, - execCommand = runCmd, - } = params; + const { session } = params; if (session.device.platform !== 'android' || !session.recording) { return 'absent'; } try { - const nodes = await readAndroidSnapshotNodes(session, snapshotAndroidUi); + const nodes = await readAndroidSnapshotNodes(session); const closeAppButton = findCloseAppButton(nodes); if (!closeAppButton?.rect) { return 'absent'; } const { x, y } = centerOfRect(closeAppButton.rect); - const tapResult = await execCommand( + const tapResult = await runCmd( 'adb', adbArgs(session.device, [ 'shell', @@ -66,7 +56,7 @@ export async function recoverAndroidBlockingSystemDialog(params: { return 'failed'; } - const dismissed = await waitForBlockingDialogToDismiss(session, snapshotAndroidUi); + const dismissed = await waitForBlockingDialogToDismiss(session); if (!dismissed) { emitDiagnostic({ level: 'warn', @@ -80,12 +70,8 @@ export async function recoverAndroidBlockingSystemDialog(params: { } if (session.appBundleId) { - await reopenAndroidApp(session.device, session.appBundleId); - const focused = await waitForFocusedAndroidApp( - session, - session.appBundleId, - readAndroidAppState, - ); + await openAndroidApp(session.device, session.appBundleId); + const focused = await waitForFocusedAndroidApp(session, session.appBundleId); if (!focused) { emitDiagnostic({ level: 'warn', @@ -126,11 +112,8 @@ export async function recoverAndroidBlockingSystemDialog(params: { } } -async function readAndroidSnapshotNodes( - session: SessionState, - snapshotAndroidUi: typeof snapshotAndroid, -): Promise { - const rawSnapshot = await snapshotAndroidUi(session.device, { +async function readAndroidSnapshotNodes(session: SessionState): Promise { + const rawSnapshot = await snapshotAndroid(session.device, { interactiveOnly: false, compact: false, }); @@ -147,34 +130,30 @@ function findCloseAppButton(nodes: SnapshotNode[]): SnapshotNode | undefined { }); } -async function waitForBlockingDialogToDismiss( - session: SessionState, - snapshotAndroidUi: typeof snapshotAndroid, -): Promise { +async function waitForBlockingDialogToDismiss(session: SessionState): Promise { for (let attempt = 0; attempt < ANDROID_MODAL_POLL_ATTEMPTS; attempt += 1) { - const nodes = await readAndroidSnapshotNodes(session, snapshotAndroidUi); + const nodes = await readAndroidSnapshotNodes(session); if (!containsBlockingDialog(nodes)) { return true; } await sleep(ANDROID_MODAL_POLL_MS); } - const nodes = await readAndroidSnapshotNodes(session, snapshotAndroidUi); + const nodes = await readAndroidSnapshotNodes(session); return !containsBlockingDialog(nodes); } async function waitForFocusedAndroidApp( session: SessionState, appBundleId: string, - readAndroidAppState: typeof getAndroidAppState, ): Promise { for (let attempt = 0; attempt < ANDROID_MODAL_POLL_ATTEMPTS; attempt += 1) { - const state = await readAndroidAppState(session.device); + const state = await getAndroidAppState(session.device); if (state.package === appBundleId) { return true; } await sleep(ANDROID_MODAL_POLL_MS); } - const state = await readAndroidAppState(session.device); + const state = await getAndroidAppState(session.device); return state.package === appBundleId; } diff --git a/src/daemon/handlers/__tests__/find.test.ts b/src/daemon/handlers/__tests__/find.vitest.ts similarity index 64% rename from src/daemon/handlers/__tests__/find.test.ts rename to src/daemon/handlers/__tests__/find.vitest.ts index 2925fb817..f7fc8cf4a 100644 --- a/src/daemon/handlers/__tests__/find.test.ts +++ b/src/daemon/handlers/__tests__/find.vitest.ts @@ -1,15 +1,28 @@ -import test, { type TestContext } from 'node:test'; -import assert from 'node:assert/strict'; +import { test, expect, vi } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { parseFindArgs, handleFindCommands } from '../find.ts'; -import { AppError } from '../../../utils/errors.ts'; import { SessionStore } from '../../session-store.ts'; import type { SessionState } from '../../types.ts'; -import type { DaemonRequest } from '../../types.ts'; +import type { DaemonRequest, DaemonResponse } from '../../types.ts'; import { withMockedMacOsHelper } from '../../../platforms/ios/__tests__/macos-helper-test-utils.ts'; +vi.mock('../../../core/dispatch.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + dispatchCommand: vi.fn(async (_device: unknown, command: string) => { + return command === 'snapshot' ? { nodes: [] } : {}; + }), + resolveTargetDevice: actual.resolveTargetDevice, + }; +}); + +import { dispatchCommand } from '../../../core/dispatch.ts'; + +const mockDispatch = vi.mocked(dispatchCommand); + function makeSessionStore(): SessionStore { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-find-handler-')); return new SessionStore(path.join(root, 'sessions')); @@ -66,6 +79,13 @@ async function runFindClickScenario(options: { const sessionName = 'default'; sessionStore.set(sessionName, makeSession(sessionName)); + mockDispatch.mockImplementation(async (_device, command) => { + if (command === 'snapshot') { + return { nodes: options.nodes }; + } + return {}; + }); + const invokeCalls: DaemonRequest[] = []; const response = await handleFindCommands({ req: { @@ -81,116 +101,108 @@ async function runFindClickScenario(options: { invoke: async (req) => { invokeCalls.push(req); const data = options.invoke ? await options.invoke(req) : {}; - return { ok: true, data }; - }, - dispatch: async (_device, command) => { - if (command === 'snapshot') { - return { nodes: options.nodes }; - } - return {}; + return { ok: true, data } as DaemonResponse; }, }); - assert.ok(response, 'expected a response'); - return { response, invokeCalls }; + expect(response).toBeTruthy(); + return { response: response!, invokeCalls }; } test('parseFindArgs defaults to click with any locator', () => { const parsed = parseFindArgs(['Login']); - assert.equal(parsed.locator, 'any'); - assert.equal(parsed.query, 'Login'); - assert.equal(parsed.action, 'click'); + expect(parsed.locator).toBe('any'); + expect(parsed.query).toBe('Login'); + expect(parsed.action).toBe('click'); }); test('parseFindArgs supports explicit locator and fill payload', () => { const parsed = parseFindArgs(['label', 'Email', 'fill', 'user@example.com']); - assert.equal(parsed.locator, 'label'); - assert.equal(parsed.query, 'Email'); - assert.equal(parsed.action, 'fill'); - assert.equal(parsed.value, 'user@example.com'); + expect(parsed.locator).toBe('label'); + expect(parsed.query).toBe('Email'); + expect(parsed.action).toBe('fill'); + expect(parsed.value).toBe('user@example.com'); }); test('parseFindArgs parses wait timeout', () => { const parsed = parseFindArgs(['text', 'Settings', 'wait', '2500']); - assert.equal(parsed.locator, 'text'); - assert.equal(parsed.action, 'wait'); - assert.equal(parsed.timeoutMs, 2500); + expect(parsed.locator).toBe('text'); + expect(parsed.action).toBe('wait'); + expect(parsed.timeoutMs).toBe(2500); }); test('parseFindArgs parses get text', () => { const parsed = parseFindArgs(['label', 'Price', 'get', 'text']); - assert.equal(parsed.locator, 'label'); - assert.equal(parsed.query, 'Price'); - assert.equal(parsed.action, 'get_text'); + expect(parsed.locator).toBe('label'); + expect(parsed.query).toBe('Price'); + expect(parsed.action).toBe('get_text'); }); test('parseFindArgs parses get attrs', () => { const parsed = parseFindArgs(['id', 'btn-1', 'get', 'attrs']); - assert.equal(parsed.locator, 'id'); - assert.equal(parsed.query, 'btn-1'); - assert.equal(parsed.action, 'get_attrs'); + expect(parsed.locator).toBe('id'); + expect(parsed.query).toBe('btn-1'); + expect(parsed.action).toBe('get_attrs'); }); test('parseFindArgs rejects invalid get sub-action', () => { - assert.throws( - () => parseFindArgs(['text', 'Settings', 'get', 'foo']), - (err: unknown) => - err instanceof AppError && - err.code === 'INVALID_ARGS' && - err.message.includes('find get only supports text or attrs'), + expect(() => parseFindArgs(['text', 'Settings', 'get', 'foo'])).toThrow( + expect.objectContaining({ + code: 'INVALID_ARGS', + message: expect.stringContaining('find get only supports text or attrs'), + }), ); }); test('parseFindArgs parses type action with value', () => { const parsed = parseFindArgs(['label', 'Name', 'type', 'Jane']); - assert.equal(parsed.locator, 'label'); - assert.equal(parsed.query, 'Name'); - assert.equal(parsed.action, 'type'); - assert.equal(parsed.value, 'Jane'); + expect(parsed.locator).toBe('label'); + expect(parsed.query).toBe('Name'); + expect(parsed.action).toBe('type'); + expect(parsed.value).toBe('Jane'); }); test('parseFindArgs joins multi-word fill value', () => { const parsed = parseFindArgs(['label', 'Bio', 'fill', 'hello', 'world']); - assert.equal(parsed.action, 'fill'); - assert.equal(parsed.value, 'hello world'); + expect(parsed.action).toBe('fill'); + expect(parsed.value).toBe('hello world'); }); test('parseFindArgs joins multi-word type value', () => { const parsed = parseFindArgs(['label', 'Bio', 'type', 'hello', 'world']); - assert.equal(parsed.action, 'type'); - assert.equal(parsed.value, 'hello world'); + expect(parsed.action).toBe('type'); + expect(parsed.value).toBe('hello world'); }); test('parseFindArgs wait without timeout leaves timeoutMs undefined', () => { const parsed = parseFindArgs(['text', 'Loading', 'wait']); - assert.equal(parsed.action, 'wait'); - assert.equal(parsed.timeoutMs, undefined); + expect(parsed.action).toBe('wait'); + expect(parsed.timeoutMs).toBeUndefined(); }); test('parseFindArgs wait with non-numeric timeout leaves timeoutMs undefined', () => { const parsed = parseFindArgs(['text', 'Loading', 'wait', 'abc']); - assert.equal(parsed.action, 'wait'); - assert.equal(parsed.timeoutMs, undefined); + expect(parsed.action).toBe('wait'); + expect(parsed.timeoutMs).toBeUndefined(); }); test('parseFindArgs throws on unsupported action', () => { - assert.throws( - () => parseFindArgs(['text', 'OK', 'swipe']), - (err: unknown) => - err instanceof AppError && - err.code === 'INVALID_ARGS' && - err.message.includes('Unsupported find action: swipe'), + expect(() => parseFindArgs(['text', 'OK', 'swipe'])).toThrow( + expect.objectContaining({ + code: 'INVALID_ARGS', + message: expect.stringContaining('Unsupported find action: swipe'), + }), ); }); test('parseFindArgs with bare locator yields empty query', () => { const parsed = parseFindArgs(['text']); - assert.equal(parsed.locator, 'text'); - assert.equal(parsed.query, ''); - assert.equal(parsed.action, 'click'); + expect(parsed.locator).toBe('text'); + expect(parsed.query).toBe(''); + expect(parsed.action).toBe('click'); }); -test('handleFindCommands click returns deterministic metadata across locator variants', async (t: TestContext) => { +test('handleFindCommands click returns deterministic metadata across locator variants', async () => { const hittableParentNoRect = { index: 0, type: 'View', hittable: true, depth: 0 }; const nonHittableChildWithRect = { index: 1, @@ -202,16 +214,7 @@ test('handleFindCommands click returns deterministic metadata across locator var parentIndex: 0, }; - const scenarios: Array<{ - label: string; - positionals: string[]; - nodes: Array>; - invoke?: (req: DaemonRequest) => Promise>; - expectedKeys: string[]; - expectedLocator: string; - expectedQuery: string; - expectedCoordinates?: { x: number; y: number }; - }> = [ + const scenarios = [ { label: 'returns deterministic matched-target metadata', positionals: ['Increment', 'click'], @@ -243,27 +246,25 @@ test('handleFindCommands click returns deterministic metadata across locator var ]; for (const scenario of scenarios) { - await t.test(scenario.label, async () => { - const { response, invokeCalls } = await runFindClickScenario(scenario); - assert.ok(response.ok, 'expected success'); - - const data = response.data as Record; - assert.deepEqual(Object.keys(data).sort(), scenario.expectedKeys); - assert.equal(data.ref, '@e1', 'ref must match the resolved snapshot node'); - assert.equal(data.locator, scenario.expectedLocator); - assert.equal(data.query, scenario.expectedQuery); - - if (scenario.expectedCoordinates) { - assert.equal(data.x, scenario.expectedCoordinates.x); - assert.equal(data.y, scenario.expectedCoordinates.y); - } else { - assert.equal(Object.hasOwn(data, 'x'), false); - assert.equal(Object.hasOwn(data, 'y'), false); - } + const { response, invokeCalls } = await runFindClickScenario(scenario); + expect(response.ok, scenario.label).toBe(true); + if (!response.ok) return; + const data = response.data as Record; + expect(Object.keys(data).sort()).toEqual(scenario.expectedKeys); + expect(data.ref).toBe('@e1'); + expect(data.locator).toBe(scenario.expectedLocator); + expect(data.query).toBe(scenario.expectedQuery); - assert.equal(invokeCalls.length, 1); - assert.equal(invokeCalls[0].positionals?.[0], '@e1'); - }); + if (scenario.expectedCoordinates) { + expect(data.x).toBe(scenario.expectedCoordinates.x); + expect(data.y).toBe(scenario.expectedCoordinates.y); + } else { + expect(Object.hasOwn(data, 'x')).toBe(false); + expect(Object.hasOwn(data, 'y')).toBe(false); + } + + expect(invokeCalls.length).toBe(1); + expect(invokeCalls[0].positionals?.[0]).toBe('@e1'); } }); @@ -286,6 +287,13 @@ test('handleFindCommands uses helper-backed snapshots for macOS desktop sessions sessionStore.set(sessionName, makeMacOsSession(sessionName)); let snapshotDispatchCalls = 0; + mockDispatch.mockImplementation(async (_device, command) => { + if (command === 'snapshot') { + snapshotDispatchCalls += 1; + } + return {}; + }); + try { const response = await handleFindCommands({ req: { @@ -298,19 +306,13 @@ test('handleFindCommands uses helper-backed snapshots for macOS desktop sessions sessionName, logPath: '/tmp/test.log', sessionStore, - invoke: async () => ({ ok: true }), - dispatch: async (_device, command) => { - if (command === 'snapshot') { - snapshotDispatchCalls += 1; - } - return {}; - }, + invoke: async () => ({ ok: true }) as DaemonResponse, }); - assert.equal(response?.ok, true); - assert.equal(snapshotDispatchCalls, 0); + expect(response?.ok).toBe(true); + expect(snapshotDispatchCalls).toBe(0); const logged = await fs.promises.readFile(argsLogPath, 'utf8'); - assert.equal(logged, 'snapshot\n--surface\ndesktop\n'); + expect(logged).toBe('snapshot\n--surface\ndesktop\n'); } finally { if (previousArgsFile === undefined) delete process.env.AGENT_DEVICE_TEST_ARGS_FILE; else process.env.AGENT_DEVICE_TEST_ARGS_FILE = previousArgsFile; diff --git a/src/daemon/handlers/__tests__/install-source.test.ts b/src/daemon/handlers/__tests__/install-source.vitest.ts similarity index 70% rename from src/daemon/handlers/__tests__/install-source.test.ts rename to src/daemon/handlers/__tests__/install-source.vitest.ts index e95ea200b..80a1ea914 100644 --- a/src/daemon/handlers/__tests__/install-source.test.ts +++ b/src/daemon/handlers/__tests__/install-source.vitest.ts @@ -1,5 +1,4 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; +import { test, expect, vi } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -9,6 +8,31 @@ import { SessionStore } from '../../session-store.ts'; import { trackUploadedArtifact } from '../../upload-registry.ts'; import type { DaemonRequest, SessionState } from '../../types.ts'; +vi.mock('../../device-ready.ts', () => ({ + ensureDeviceReady: vi.fn(async () => {}), +})); + +vi.mock('../../../core/dispatch.ts', () => ({ + resolveTargetDevice: vi.fn(), +})); + +vi.mock('../../request-cancel.ts', () => ({ + getRequestSignal: vi.fn(() => undefined), +})); + +vi.mock('../../../platforms/android/install-artifact.ts', () => ({ + prepareAndroidInstallArtifact: vi.fn(async () => ({ + installablePath: '/tmp/materialized/app.apk', + packageName: undefined, + cleanup: async () => {}, + })), +})); + +vi.mock('../../../platforms/android/index.ts', () => ({ + installAndroidInstallablePathAndResolvePackageName: vi.fn(async () => 'com.example.app'), + inferAndroidAppName: vi.fn(() => 'App'), +})); + function makeRequest(meta?: DaemonRequest['meta']): DaemonRequest { return { token: 't', @@ -56,11 +80,13 @@ test('resolveInstallSource uses uploaded artifact path for uploaded path sources }), ); - assert.equal(resolved.source.kind, 'path'); - assert.equal(resolved.source.path, artifactPath); + expect(resolved.source.kind).toBe('path'); + if (resolved.source.kind === 'path') { + expect(resolved.source.path).toBe(artifactPath); + } resolved.cleanup(); - assert.equal(fs.existsSync(tempRoot), false); + expect(fs.existsSync(tempRoot)).toBe(false); }); test('resolveInstallSource leaves URL sources unchanged even when upload metadata exists', () => { @@ -75,7 +101,7 @@ test('resolveInstallSource leaves URL sources unchanged even when upload metadat }), ); - assert.deepEqual(resolved.source, { + expect(resolved.source).toEqual({ kind: 'url', url: 'https://example.com/app.apk', headers: {}, @@ -98,19 +124,9 @@ test('install_from_source returns Android package identity resolved after instal }), sessionName: session.name, sessionStore, - deps: { - resolveInstallDevice: async () => session.device, - prepareAndroidInstallArtifact: async () => ({ - installablePath: '/tmp/materialized/app.apk', - packageName: undefined, - cleanup: async () => {}, - }), - installAndroidInstallablePathAndResolvePackageName: async () => 'com.example.app', - inferAndroidAppName: () => 'App', - }, }); - assert.deepEqual(response, { + expect(response).toEqual({ ok: true, data: { packageName: 'com.example.app', @@ -118,7 +134,7 @@ test('install_from_source returns Android package identity resolved after instal launchTarget: 'com.example.app', }, }); - assert.deepEqual(session.actions.at(-1)?.result, { + expect(session.actions.at(-1)?.result).toEqual({ packageName: 'com.example.app', appName: 'App', launchTarget: 'com.example.app', @@ -126,6 +142,10 @@ test('install_from_source returns Android package identity resolved after instal }); test('install_from_source returns an error when Android package identity cannot be resolved', async () => { + const { installAndroidInstallablePathAndResolvePackageName } = + await import('../../../platforms/android/index.ts'); + vi.mocked(installAndroidInstallablePathAndResolvePackageName).mockResolvedValueOnce(undefined); + const sessionStore = makeSessionStore(); const session = makeAndroidSession('default'); sessionStore.set(session.name, session); @@ -140,19 +160,11 @@ test('install_from_source returns an error when Android package identity cannot }), sessionName: session.name, sessionStore, - deps: { - resolveInstallDevice: async () => session.device, - prepareAndroidInstallArtifact: async () => ({ - installablePath: '/tmp/materialized/app.apk', - packageName: undefined, - cleanup: async () => {}, - }), - installAndroidInstallablePathAndResolvePackageName: async () => undefined, - inferAndroidAppName: () => 'App', - }, }); - assert.equal(response.ok, false); - assert.equal(response.error?.code, 'COMMAND_FAILED'); - assert.match(response.error?.message ?? '', /identity could not be resolved/i); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error.code).toBe('COMMAND_FAILED'); + expect(response.error.message).toMatch(/identity could not be resolved/i); + } }); diff --git a/src/daemon/handlers/__tests__/record-trace.test.ts b/src/daemon/handlers/__tests__/record-trace.test.ts index 6e21cc8cf..1f46486d5 100644 --- a/src/daemon/handlers/__tests__/record-trace.test.ts +++ b/src/daemon/handlers/__tests__/record-trace.test.ts @@ -132,7 +132,7 @@ function makeRunnerRecordingDeps( runIosRunnerCommand, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }; @@ -147,7 +147,7 @@ function makeRecordDeps(overrides: Partial = {}): RecordTraceDe runIosRunnerCommand: async () => ({}), waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, ...overrides, @@ -395,7 +395,7 @@ test('record start rejects invalid fps value', async () => { }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -425,7 +425,7 @@ test('record start on iOS device requires active app session context', async () }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -456,7 +456,7 @@ test('record start returns structured error when iOS runner start fails', async }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -497,7 +497,7 @@ test('record start recovers from stale iOS runner recording state', async () => }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -545,7 +545,7 @@ test('record start does not stop recording owned by another session during desyn }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -591,7 +591,7 @@ test('record stop clears iOS runner recording state when runner stop fails', asy }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, @@ -629,10 +629,6 @@ test('record stop trims iOS device recordings from target app readiness before o trimRecordingStart: async ({ videoPath, trimStartMs }) => { lifecycleCalls.push(`trim:${videoPath}:${trimStartMs}`); }, - writeRecordingTelemetry: ({ videoPath, events }) => { - lifecycleCalls.push(`telemetry:${videoPath}:${events.length}`); - return deriveRecordingTelemetryPath(videoPath); - }, overlayRecordingTouches: async ({ videoPath, telemetryPath }) => { lifecycleCalls.push(`overlay:${videoPath}:${telemetryPath}`); }, @@ -640,7 +636,7 @@ test('record stop trims iOS device recordings from target app readiness before o }); assert.equal(response?.ok, true); - const expectedLifecycleCalls = ['trim:/tmp/device.mp4:3250', 'telemetry:/tmp/device.mp4:1']; + const expectedLifecycleCalls = ['trim:/tmp/device.mp4:3250']; if (!overlaySupportWarning) { expectedLifecycleCalls.push( `overlay:/tmp/device.mp4:${deriveRecordingTelemetryPath('/tmp/device.mp4')}`, @@ -747,7 +743,7 @@ test('record stop keeps iOS simulator video when overlay export fails', async () runIosRunnerCommand: async () => ({}), waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => { throw new Error('swift export failed'); @@ -767,7 +763,7 @@ test('record stop keeps iOS simulator video when overlay export fails', async () runIosRunnerCommand: async () => ({}), waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => { throw new Error('swift export failed'); @@ -814,7 +810,7 @@ test('record start does not fail when iOS simulator runner warm-up fails', async }, waitForStableFile: async () => {}, isPlayableVideo: async () => true, - writeRecordingTelemetry: ({ videoPath }) => deriveRecordingTelemetryPath(videoPath), + trimRecordingStart: async () => {}, overlayRecordingTouches: async () => {}, }, diff --git a/src/daemon/handlers/__tests__/session-runtime-command.vitest.ts b/src/daemon/handlers/__tests__/session-runtime-command.vitest.ts new file mode 100644 index 000000000..17b1c8a66 --- /dev/null +++ b/src/daemon/handlers/__tests__/session-runtime-command.vitest.ts @@ -0,0 +1,64 @@ +import { test, expect, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { SessionStore } from '../../session-store.ts'; +import type { SessionState } from '../../types.ts'; + +vi.mock('../../runtime-hints.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + clearRuntimeHintsFromApp: vi.fn(async () => {}), + }; +}); + +import { handleRuntimeCommand } from '../session-runtime-command.ts'; +import { clearRuntimeHintsFromApp } from '../../runtime-hints.ts'; + +function makeSessionStore(): SessionStore { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runtime-cmd-')); + return new SessionStore(path.join(root, 'sessions')); +} + +test('runtime clear removes applied transport hints for the active app', async () => { + const sessionStore = makeSessionStore(); + const sessionName = 'runtime-clear-active'; + sessionStore.setRuntimeHints(sessionName, { + platform: 'android', + metroHost: '10.0.0.10', + metroPort: 8081, + }); + sessionStore.set(sessionName, { + name: sessionName, + createdAt: Date.now(), + actions: [], + device: { + platform: 'android', + id: 'emulator-5554', + name: 'Pixel', + kind: 'emulator', + booted: true, + }, + appBundleId: 'com.example.demo', + } as SessionState); + + const response = await handleRuntimeCommand({ + req: { + token: 't', + session: sessionName, + command: 'runtime', + positionals: ['clear'], + flags: {}, + }, + sessionName, + sessionStore, + }); + + expect(response.ok).toBe(true); + expect(vi.mocked(clearRuntimeHintsFromApp)).toHaveBeenCalledWith({ + device: expect.objectContaining({ id: 'emulator-5554' }), + appId: 'com.example.demo', + }); + expect(sessionStore.getRuntimeHints(sessionName)).toBeUndefined(); +}); diff --git a/src/daemon/handlers/find.ts b/src/daemon/handlers/find.ts index 30e982ac9..37fc226b9 100644 --- a/src/daemon/handlers/find.ts +++ b/src/daemon/handlers/find.ts @@ -17,10 +17,8 @@ export async function handleFindCommands(params: { logPath: string; sessionStore: SessionStore; invoke: (req: DaemonRequest) => Promise; - dispatch?: typeof dispatchCommand; }): Promise { const { req, sessionName, logPath, sessionStore, invoke } = params; - const dispatch = params.dispatch ?? dispatchCommand; const command = req.command; if (command !== 'find') return null; @@ -64,7 +62,7 @@ export async function handleFindCommands(params: { return { nodes: lastNodes }; } const { snapshot } = await captureSnapshot({ - dispatchSnapshotCommand: dispatch, + dispatchSnapshotCommand: dispatchCommand, device, session, flags: { @@ -169,7 +167,7 @@ export async function handleFindCommands(params: { surface: session?.surface, contextFromFlags: (flags, appBundleId, traceLogPath) => contextFromFlags(logPath, flags, appBundleId, traceLogPath), - dispatch, + dispatch: dispatchCommand, }); if (session) { sessionStore.recordAction(session, { @@ -247,7 +245,7 @@ export async function handleFindCommands(params: { error: { code: 'COMMAND_FAILED', message: 'matched element has no bounds' }, }; } - const response = await dispatch( + const response = await dispatchCommand( device, 'focus', [String(coords.x), String(coords.y)], @@ -277,10 +275,10 @@ export async function handleFindCommands(params: { error: { code: 'COMMAND_FAILED', message: 'matched element has no bounds' }, }; } - await dispatch(device, 'focus', [String(coords.x), String(coords.y)], req.flags?.out, { + await dispatchCommand(device, 'focus', [String(coords.x), String(coords.y)], req.flags?.out, { ...contextFromFlags(logPath, req.flags, session?.appBundleId, session?.trace?.outPath), }); - const response = await dispatch(device, 'type', [value], req.flags?.out, { + const response = await dispatchCommand(device, 'type', [value], req.flags?.out, { ...contextFromFlags(logPath, req.flags, session?.appBundleId, session?.trace?.outPath), }); if (session) { diff --git a/src/daemon/handlers/install-source.ts b/src/daemon/handlers/install-source.ts index 7216905f3..4ef0b5732 100644 --- a/src/daemon/handlers/install-source.ts +++ b/src/daemon/handlers/install-source.ts @@ -9,24 +9,8 @@ import { import { resolveInstallSource } from '../install-source-resolution.ts'; import { SessionStore } from '../session-store.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; -import type { MaterializeInstallSource } from '../../platforms/install-source.ts'; import { AppError, normalizeError } from '../../utils/errors.ts'; -type PreparedIosInstallArtifact = { - archivePath?: string; - installablePath: string; - bundleId?: string; - appName?: string; - cleanup: () => Promise; -}; - -type PreparedAndroidInstallArtifact = { - archivePath?: string; - installablePath: string; - packageName?: string; - cleanup: () => Promise; -}; - function normalizePlatform(platform: CommandFlags['platform']): 'ios' | 'android' | undefined { return platform === 'ios' || platform === 'android' ? platform : undefined; } @@ -75,35 +59,13 @@ export async function handleInstallFromSourceCommand(params: { req: DaemonRequest; sessionName: string; sessionStore: SessionStore; - deps?: { - resolveInstallDevice?: typeof resolveInstallDevice; - getRequestSignal?: typeof getRequestSignal; - prepareIosInstallArtifact?: ( - source: MaterializeInstallSource, - options?: { signal?: AbortSignal }, - ) => Promise; - installIosInstallablePath?: ( - device: SessionState['device'], - installablePath: string, - ) => Promise; - prepareAndroidInstallArtifact?: ( - source: MaterializeInstallSource, - options?: { signal?: AbortSignal; resolveIdentity?: boolean }, - ) => Promise; - installAndroidInstallablePathAndResolvePackageName?: ( - device: SessionState['device'], - installablePath: string, - packageNameHint?: string, - ) => Promise; - inferAndroidAppName?: (packageName: string) => string; - }; }): Promise { - const { req, sessionName, sessionStore, deps } = params; + const { req, sessionName, sessionStore } = params; const session = sessionStore.get(sessionName); try { const resolvedSource = resolveInstallSource(req); const retention = resolveRetainMaterializedPaths(req); - const device = await (deps?.resolveInstallDevice ?? resolveInstallDevice)({ + const device = await resolveInstallDevice({ session, flags: req.flags, }); @@ -117,14 +79,10 @@ export async function handleInstallFromSourceCommand(params: { }; } - const requestSignal = (deps?.getRequestSignal ?? getRequestSignal)(req.meta?.requestId); + const requestSignal = getRequestSignal(req.meta?.requestId); if (device.platform === 'ios') { - const installIosInstallablePath = - deps?.installIosInstallablePath ?? - (await import('../../platforms/ios/index.ts')).installIosInstallablePath; - const prepareIosInstallArtifact = - deps?.prepareIosInstallArtifact ?? - (await import('../../platforms/ios/install-artifact.ts')).prepareIosInstallArtifact; + const { installIosInstallablePath } = await import('../../platforms/ios/index.ts'); + const { prepareIosInstallArtifact } = await import('../../platforms/ios/install-artifact.ts'); const prepared = await prepareIosInstallArtifact(resolvedSource.source, { signal: requestSignal, }); @@ -182,13 +140,10 @@ export async function handleInstallFromSourceCommand(params: { } } - const prepareAndroidInstallArtifact = - deps?.prepareAndroidInstallArtifact ?? - (await import('../../platforms/android/install-artifact.ts')).prepareAndroidInstallArtifact; - const installAndroidInstallablePathAndResolvePackageName = - deps?.installAndroidInstallablePathAndResolvePackageName ?? - (await import('../../platforms/android/index.ts')) - .installAndroidInstallablePathAndResolvePackageName; + const { prepareAndroidInstallArtifact } = + await import('../../platforms/android/install-artifact.ts'); + const { installAndroidInstallablePathAndResolvePackageName } = + await import('../../platforms/android/index.ts'); const prepared = await prepareAndroidInstallArtifact(resolvedSource.source, { signal: requestSignal, }); @@ -214,9 +169,7 @@ export async function handleInstallFromSourceCommand(params: { 'Installed Android app identity could not be resolved from the artifact or device state', ); } - const inferAndroidAppName = - deps?.inferAndroidAppName ?? - (await import('../../platforms/android/index.ts')).inferAndroidAppName; + const { inferAndroidAppName } = await import('../../platforms/android/index.ts'); const appName = inferAndroidAppName(packageName); const result = { ...(retained?.archivePath ? { archivePath: retained.archivePath } : {}), diff --git a/src/daemon/handlers/record-trace-android.ts b/src/daemon/handlers/record-trace-android.ts index 18e6a7aff..1a03f0454 100644 --- a/src/daemon/handlers/record-trace-android.ts +++ b/src/daemon/handlers/record-trace-android.ts @@ -374,7 +374,6 @@ export async function stopAndroidRecording(params: { persistRecordingTelemetry({ recording, - writeTelemetry: deps.writeRecordingTelemetry, }); if (recording.showTouches && recording.telemetryPath) { const overlaySupportWarning = getRecordingOverlaySupportWarning(); diff --git a/src/daemon/handlers/record-trace-ios.ts b/src/daemon/handlers/record-trace-ios.ts index 5307b3f0d..9c150367c 100644 --- a/src/daemon/handlers/record-trace-ios.ts +++ b/src/daemon/handlers/record-trace-ios.ts @@ -336,7 +336,6 @@ export async function stopIosDeviceRecording(params: { const telemetryPath = persistRecordingTelemetry({ recording, trimStartMs, - writeTelemetry: deps.writeRecordingTelemetry, }); if (recording.showTouches) { @@ -392,7 +391,6 @@ export async function stopMacOsRecording(params: { const telemetryPath = persistRecordingTelemetry({ recording, - writeTelemetry: deps.writeRecordingTelemetry, }); if (recording.showTouches) { diff --git a/src/daemon/handlers/record-trace-recording.ts b/src/daemon/handlers/record-trace-recording.ts index 621689dcd..e8bc75dcc 100644 --- a/src/daemon/handlers/record-trace-recording.ts +++ b/src/daemon/handlers/record-trace-recording.ts @@ -13,11 +13,7 @@ import type { } from '../types.ts'; import { runCmd, runCmdBackground } from '../../utils/exec.ts'; import { isPlayableVideo, waitForStableFile } from '../../utils/video.ts'; -import { - deriveRecordingTelemetryPath, - persistRecordingTelemetry, - writeRecordingTelemetry, -} from '../recording-telemetry.ts'; +import { deriveRecordingTelemetryPath, persistRecordingTelemetry } from '../recording-telemetry.ts'; import { runIosRunnerCommand } from '../../platforms/ios/runner-client.ts'; import { getRecordingOverlaySupportWarning, @@ -47,7 +43,6 @@ export type RecordTraceDeps = { runIosRunnerCommand: typeof runIosRunnerCommand; waitForStableFile: typeof waitForStableFile; isPlayableVideo: typeof isPlayableVideo; - writeRecordingTelemetry: typeof writeRecordingTelemetry; trimRecordingStart: typeof trimRecordingStart; overlayRecordingTouches: typeof overlayRecordingTouches; }; @@ -67,7 +62,6 @@ export function buildRecordTraceDeps(overrides?: Partial): Reco runIosRunnerCommand, waitForStableFile, isPlayableVideo, - writeRecordingTelemetry, trimRecordingStart, overlayRecordingTouches, ...overrides, @@ -306,7 +300,6 @@ async function stopNonRunnerRecording(params: { const telemetryPath = persistRecordingTelemetry({ recording, - writeTelemetry: deps.writeRecordingTelemetry, }); if (recording.showTouches) { diff --git a/src/daemon/recording-telemetry.ts b/src/daemon/recording-telemetry.ts index bdb983421..11be65fff 100644 --- a/src/daemon/recording-telemetry.ts +++ b/src/daemon/recording-telemetry.ts @@ -72,10 +72,9 @@ export function writeRecordingTelemetry(params: { export function persistRecordingTelemetry(params: { recording: RecordingTelemetryState; trimStartMs?: number; - writeTelemetry?: typeof writeRecordingTelemetry; }): string { - const { recording, trimStartMs, writeTelemetry = writeRecordingTelemetry } = params; - const telemetryPath = writeTelemetry({ + const { recording, trimStartMs } = params; + const telemetryPath = writeRecordingTelemetry({ videoPath: recording.outPath, events: recording.gestureEvents, trimStartMs, diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index 9862a0bbc..2a8d13c50 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -38,9 +38,7 @@ import { recordTouchVisualizationEvent, } from './recording-gestures.ts'; import { recoverAndroidBlockingSystemDialog } from './android-system-dialog.ts'; -import { snapshotAndroid, openAndroidApp, getAndroidAppState } from '../platforms/android/index.ts'; import { getRunnerSessionSnapshot } from '../platforms/ios/runner-client.ts'; -import { runCmd } from '../utils/exec.ts'; const selectorValidationExemptCommands = new Set([ 'session_list', @@ -247,10 +245,6 @@ export type RequestRouterDeps = { fileName?: string; }) => string; dispatchCommand?: typeof dispatchCommand; - snapshotAndroidUi?: typeof snapshotAndroid; - reopenAndroidApp?: typeof openAndroidApp; - readAndroidAppState?: typeof getAndroidAppState; - execCommand?: typeof runCmd; }; export function createRequestHandler( @@ -258,10 +252,6 @@ export function createRequestHandler( ): (req: DaemonRequest) => Promise { const { logPath, token, sessionStore, leaseRegistry, trackDownloadableArtifact } = deps; const dispatch = deps.dispatchCommand ?? dispatchCommand; - const snapshotAndroidUi = deps.snapshotAndroidUi ?? snapshotAndroid; - const reopenAndroidApp = deps.reopenAndroidApp ?? openAndroidApp; - const readAndroidAppState = deps.readAndroidAppState ?? getAndroidAppState; - const execCommand = deps.execCommand ?? runCmd; async function handleRequest(req: DaemonRequest): Promise { const normalizedReq = normalizeAliasedCommands(req); @@ -405,10 +395,6 @@ export function createRequestHandler( if (session.device.platform === 'android' && session.recording && command !== 'record') { const androidRecoveryResult = await recoverAndroidBlockingSystemDialog({ session, - snapshotAndroidUi, - reopenAndroidApp, - readAndroidAppState, - execCommand, }); if (androidRecoveryResult === 'failed') { return finalize({ diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..7ecf517e7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.vitest.ts'], + }, +});