From 82bc95ae7815350011ef2bf15d512ccb508d6ca2 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 15 Jul 2026 18:04:32 +0545 Subject: [PATCH 1/5] chore(OUT-3986): add msw dev dependency Mock Service Worker (2.x) fakes Dropbox + Copilot at the HTTP boundary for the integration test suite. Dev-only; no production dependency. Co-Authored-By: Claude Opus 4.8 --- package.json | 1 + pnpm-lock.yaml | 284 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 281 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2b36215..b17b98c 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "drizzle-kit": "^0.31.4", "husky": "^9.1.7", "lint-staged": "^16.1.5", + "msw": "^2.15.0", "server-only": "^0.0.1", "supabase": "^2.39.2", "tailwindcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1568bf5..cfaac39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: lint-staged: specifier: ^16.1.5 version: 16.1.5 + msw: + specifier: ^2.15.0 + version: 2.15.0(@types/node@20.19.11)(typescript@5.9.2) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -173,7 +176,7 @@ importers: version: 6.1.1(typescript@5.9.2)(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^4.1.5 - version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.11)(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.11)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) packages: @@ -912,6 +915,41 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -948,6 +986,10 @@ packages: '@microsoft/fetch-event-source@2.0.1': resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==} + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1062,6 +1104,18 @@ packages: cpu: [x64] os: [win32] + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.203.0': resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} engines: {node: '>=8.0.0'} @@ -2170,6 +2224,9 @@ packages: '@types/react@19.1.11': resolution: {integrity: sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -2182,6 +2239,9 @@ packages: '@types/ssh2@1.15.5': resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -2553,6 +2613,10 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -2632,6 +2696,10 @@ packages: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copilot-design-system@2.2.6: resolution: {integrity: sha512-5q20q7lbc3eoycuv1W+2jB8LwnOY5esOg4g2hALnEn3XMdPHWWj4Hcx4DIDyZaRPei1xjdB8hRLBBJnuVa400g==} peerDependencies: @@ -3056,9 +3124,18 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3182,6 +3259,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3198,6 +3279,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3305,6 +3389,9 @@ packages: resolution: {integrity: sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==} engines: {node: '>=16'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3704,6 +3791,20 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + nan@2.28.0: resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} @@ -3819,6 +3920,9 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3873,6 +3977,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4127,6 +4234,9 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -4174,6 +4284,9 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + sharp@0.34.3: resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4264,6 +4377,10 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -4277,6 +4394,9 @@ packages: streamx@2.28.0: resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -4367,6 +4487,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -4470,6 +4594,13 @@ packages: resolution: {integrity: sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==} hasBin: true + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} + + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} + hasBin: true + tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} @@ -4478,6 +4609,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -4517,6 +4652,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -4556,6 +4695,9 @@ packages: unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -5489,6 +5631,33 @@ snapshots: '@img/sharp-win32-x64@0.34.3': optional: true + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@20.19.11)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@20.19.11) + '@inquirer/type': 4.0.7(@types/node@20.19.11) + optionalDependencies: + '@types/node': 20.19.11 + + '@inquirer/core@11.2.1(@types/node@20.19.11)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@20.19.11) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 20.19.11 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@20.19.11)': + optionalDependencies: + '@types/node': 20.19.11 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5532,6 +5701,15 @@ snapshots: '@microsoft/fetch-event-source@2.0.1': {} + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -5594,6 +5772,17 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.18': optional: true + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.203.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -6804,6 +6993,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 20.19.11 + '@types/shimmer@1.2.0': {} '@types/ssh2-streams@0.1.13': @@ -6819,6 +7012,8 @@ snapshots: dependencies: '@types/node': 18.19.130 + '@types/statuses@2.0.6': {} + '@types/tedious@4.0.14': dependencies: '@types/node': 20.19.11 @@ -6838,12 +7033,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1))': + '@vitest/mocker@4.1.5(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.15.0(@types/node@20.19.11)(typescript@5.9.2) vite: 8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1) '@vitest/pretty-format@4.1.5': @@ -7218,6 +7414,8 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-width@4.1.0: {} + client-only@0.0.1: {} cliui@8.0.1: @@ -7296,6 +7494,8 @@ snapshots: cookie@0.4.2: {} + cookie@1.1.1: {} + copilot-design-system@2.2.6(@types/react-dom@19.1.7(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.7(react@19.1.7))(react@19.1.7)(typescript@5.9.2): dependencies: '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.11))(@types/react@19.1.11)(react-dom@19.1.7(react@19.1.7))(react@19.1.7) @@ -7708,8 +7908,18 @@ snapshots: fast-fifo@1.3.2: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.0.6: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -7838,6 +8048,8 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.14.2: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -7850,6 +8062,11 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.2 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -7951,6 +8168,8 @@ snapshots: is-network-error@1.1.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-obj@2.0.0: {} @@ -8276,6 +8495,33 @@ snapshots: ms@2.1.3: {} + msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@20.19.11) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/node' + + mute-stream@3.0.0: {} + nan@2.28.0: optional: true @@ -8377,6 +8623,8 @@ snapshots: dependencies: mimic-function: 5.0.1 + outvariant@1.4.3: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -8425,6 +8673,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -8715,6 +8965,8 @@ snapshots: retry@0.12.0: {} + rettime@0.11.11: {} + rfdc@1.4.1: {} rolldown@1.0.0-rc.17: @@ -8795,6 +9047,8 @@ snapshots: server-only@0.0.1: {} + set-cookie-parser@3.1.2: {} + sharp@0.34.3: dependencies: color: 4.2.3 @@ -8931,6 +9185,8 @@ snapshots: dependencies: type-fest: 0.7.1 + statuses@2.0.2: {} + std-env@3.10.0: {} std-env@4.1.0: {} @@ -8946,6 +9202,8 @@ snapshots: - bare-abort-controller - react-native-b4a + strict-event-emitter@0.5.1: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -9029,6 +9287,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tagged-tag@1.0.0: {} + tailwind-merge@2.6.0: {} tailwind-merge@3.3.1: {} @@ -9170,12 +9430,22 @@ snapshots: tlds@1.260.0: {} + tldts-core@7.4.8: {} + + tldts@7.4.8: + dependencies: + tldts-core: 7.4.8 + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.8 + tr46@0.0.3: {} tsafe@1.8.12: {} @@ -9205,6 +9475,10 @@ snapshots: type-fest@4.41.0: {} + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + typescript@5.9.2: {} uc.micro@1.0.6: {} @@ -9234,6 +9508,8 @@ snapshots: webpack-sources: 3.3.3 webpack-virtual-modules: 0.5.0 + until-async@3.0.2: {} + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0 @@ -9284,10 +9560,10 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 - vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.11)(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@20.19.11)(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/mocker': 4.1.5(msw@2.15.0(@types/node@20.19.11)(typescript@5.9.2))(vite@8.0.10(@types/node@20.19.11)(esbuild@0.25.9)(jiti@2.5.1)(terser@5.44.1)(tsx@4.20.5)(yaml@2.8.1)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 From db5e71fcebf4cbba5d028735a70eb4375c53831a Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 15 Jul 2026 18:04:52 +0545 Subject: [PATCH 2/5] test(OUT-3986): add MSW harness faking Dropbox + Copilot at HTTP boundary Fake the Dropbox (rpc + content) and Copilot hosts with MSW so integration tests drive the real SDK clients without network access: - server + base handlers (ambient OAuth refresh, empty list_folder/listFiles) - per-test override helpers (mockDropboxRpc / mockDropboxContent / mockCopilot) - error-shape builders reproducing the errors production branches on (isCopilotApiError 400/404; DropboxResponseError 409 not_found) by returning HTTP responses the real SDKs turn into those errors - cursor / nextToken pagination helpers - a self-test driving the real clients to prove every shape reproduces Wire the MSW lifecycle into the integration setup (onUnhandledRequest: 'error'), point DROPBOX_API_URL at the content host, set COPILOT_ENV=local so the Copilot SDK trusts the placeholder apiKey, and inline copilot-node-sdk in Vitest to resolve its ESM directory import. Co-Authored-By: Claude Opus 4.8 --- test/integration/setup.ts | 9 +- test/msw/errors.ts | 51 ++++++++ test/msw/handlers/copilot.ts | 7 ++ test/msw/handlers/dropbox.ts | 21 ++++ test/msw/hosts.ts | 4 + test/msw/index.ts | 5 + test/msw/msw-harness.integration.test.ts | 149 +++++++++++++++++++++++ test/msw/overrides.ts | 25 ++++ test/msw/pagination.ts | 40 ++++++ test/msw/server.ts | 5 + test/support/placeholder-env.ts | 13 +- vitest.integration.config.ts | 10 +- 12 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 test/msw/errors.ts create mode 100644 test/msw/handlers/copilot.ts create mode 100644 test/msw/handlers/dropbox.ts create mode 100644 test/msw/hosts.ts create mode 100644 test/msw/index.ts create mode 100644 test/msw/msw-harness.integration.test.ts create mode 100644 test/msw/overrides.ts create mode 100644 test/msw/pagination.ts create mode 100644 test/msw/server.ts diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 6841bcf..346baa0 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -1,5 +1,6 @@ import postgres from 'postgres' -import { afterAll, beforeEach, inject } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, inject } from 'vitest' +import { server } from '../msw/server' import { applyPlaceholderServerEnv } from '../support/placeholder-env' // Runs in every worker BEFORE any test file imports `@/db`. Point the app's DB @@ -7,6 +8,12 @@ import { applyPlaceholderServerEnv } from '../support/placeholder-env' // container, and satisfy the rest of the server-env Zod schema with placeholders. applyPlaceholderServerEnv() +// MSW fakes Dropbox + Copilot over HTTP. `error` mode flags any unmocked call; +// per-test overrides are cleared between tests. +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + // SAFETY: only ever run (and TRUNCATE) against a local Testcontainers DB. If the // injected URL is anything but localhost, refuse to start — never touch a remote // / production database. diff --git a/test/msw/errors.ts b/test/msw/errors.ts new file mode 100644 index 0000000..31e0f78 --- /dev/null +++ b/test/msw/errors.ts @@ -0,0 +1,51 @@ +import httpStatus from 'http-status' +import { HttpResponse } from 'msw' + +function statusText(status: number): string { + const text = (httpStatus as unknown as Record)[status] + return typeof text === 'string' ? text : 'Error' +} + +// Copilot: the SDK turns a 4xx JSON body into the ApiError shape isCopilotApiError checks. +export function copilotError({ status, body }: { status: number; body: Record }) { + return HttpResponse.json(body, { status, statusText: statusText(status) }) +} + +export const copilotFolderExists = () => + copilotError({ status: 400, body: { message: 'Folder already exists' } }) + +export const copilotNotFound = (body: Record = { message: 'Not found' }) => + copilotError({ status: 404, body }) + +// Dropbox: the SDK puts the JSON body on DropboxResponseError.error. +// Prod reads err.error.error.path['.tag'] and err.error.error_summary. +export function dropboxRpcError({ + status, + errorSummary, + error, +}: { + status: number + errorSummary: string + error: Record +}) { + return HttpResponse.json( + { error_summary: errorSummary, error }, + { status, statusText: statusText(status) }, + ) +} + +// not_found via the path tag (get_metadata branch). +export const dropboxGetMetadataNotFound = () => + dropboxRpcError({ + status: 409, + errorSummary: 'path/not_found/..', + error: { '.tag': 'path', path: { '.tag': 'not_found' } }, + }) + +// not_found via the error_summary prefix; path tag also set so both branches match. +export const dropboxPathLookupNotFound = () => + dropboxRpcError({ + status: 409, + errorSummary: 'path_lookup/not_found/..', + error: { '.tag': 'path', path: { '.tag': 'not_found' } }, + }) diff --git a/test/msw/handlers/copilot.ts b/test/msw/handlers/copilot.ts new file mode 100644 index 0000000..8b48cc5 --- /dev/null +++ b/test/msw/handlers/copilot.ts @@ -0,0 +1,7 @@ +import { HttpResponse, http } from 'msw' +import { COPILOT_HOST } from '../hosts' + +// Empty page by default. Override with paginateCopilotListFiles / mockCopilot. +const listFiles = http.get(`${COPILOT_HOST}/v1/files`, () => HttpResponse.json({ data: [] })) + +export const copilotBaseHandlers = [listFiles] diff --git a/test/msw/handlers/dropbox.ts b/test/msw/handlers/dropbox.ts new file mode 100644 index 0000000..2e9728b --- /dev/null +++ b/test/msw/handlers/dropbox.ts @@ -0,0 +1,21 @@ +import { HttpResponse, http } from 'msw' +import { DROPBOX_RPC_HOST } from '../hosts' + +// The SDK refreshes the token before every rpc call, so answer it here. +const oauthToken = http.post(`${DROPBOX_RPC_HOST}/oauth2/token`, () => + HttpResponse.json({ + access_token: 'test-access-token', + token_type: 'bearer', + expires_in: 14400, + }), +) + +// Empty folder by default. Override with paginateDropboxListFolder / mockDropboxRpc. +const listFolder = http.post(`${DROPBOX_RPC_HOST}/2/files/list_folder`, () => + HttpResponse.json({ entries: [], cursor: 'end', has_more: false }), +) +const listFolderContinue = http.post(`${DROPBOX_RPC_HOST}/2/files/list_folder/continue`, () => + HttpResponse.json({ entries: [], cursor: 'end', has_more: false }), +) + +export const dropboxBaseHandlers = [oauthToken, listFolder, listFolderContinue] diff --git a/test/msw/hosts.ts b/test/msw/hosts.ts new file mode 100644 index 0000000..8780a7a --- /dev/null +++ b/test/msw/hosts.ts @@ -0,0 +1,4 @@ +// Dependency-free so other files can import it without an import cycle via server.ts. +export const DROPBOX_RPC_HOST = 'https://api.dropboxapi.com' +export const DROPBOX_CONTENT_HOST = 'https://content.dropboxapi.com' +export const COPILOT_HOST = 'https://api.copilot.app' diff --git a/test/msw/index.ts b/test/msw/index.ts new file mode 100644 index 0000000..9b1d618 --- /dev/null +++ b/test/msw/index.ts @@ -0,0 +1,5 @@ +export * from './errors' +export * from './hosts' +export * from './overrides' +export * from './pagination' +export { server } from './server' diff --git a/test/msw/msw-harness.integration.test.ts b/test/msw/msw-harness.integration.test.ts new file mode 100644 index 0000000..6dbe26e --- /dev/null +++ b/test/msw/msw-harness.integration.test.ts @@ -0,0 +1,149 @@ +import { copilotApi } from 'copilot-node-sdk' +import { DropboxResponseError } from 'dropbox' +import { HttpResponse } from 'msw' +import { describe, expect, it } from 'vitest' +import { CopilotAPI, isCopilotApiError } from '@/lib/copilot/CopilotAPI' +import { DropboxClient } from '@/lib/dropbox/DropboxClient' +import { + copilotFolderExists, + copilotNotFound, + dropboxGetMetadataNotFound, + dropboxPathLookupNotFound, + mockCopilot, + mockDropboxContent, + mockDropboxRpc, + paginateCopilotListFiles, + paginateDropboxListFolder, +} from '../msw' +import { server } from '../msw/server' + +describe('MSW harness — interception', () => { + it('rejects an unhandled request (proves error mode)', async () => { + await expect(fetch('https://unregistered.example.com/x')).rejects.toThrow() + }) +}) + +describe('MSW harness — base handlers + hosts', () => { + it('serves an empty Dropbox folder listing to the real SDK (node-fetch)', async () => { + const dbx = new DropboxClient('refresh-token', null) + const entries = await dbx.getAllFilesFolders('/root', false, true) + expect(entries).toEqual([]) + }) + + it('serves an empty Copilot file page to the real SDK (undici fetch)', async () => { + const page = await new CopilotAPI('token')._listFiles('ch_1') + expect(page.data).toEqual([]) + }) + + it('routes content-host requests through an override (node-fetch manualFetch)', async () => { + mockDropboxContent('/files/download', () => + HttpResponse.json( + {}, + { status: 200, headers: { 'Dropbox-API-Result': JSON.stringify({ size: 5 }) } }, + ), + ) + const dbx = new DropboxClient('refresh-token', 'ns_1') + const { contentLength } = await dbx._downloadFile({ + urlPath: '/files/download', + filePath: '/a.txt', + rootNamespaceId: 'ns_1', + refreshToken: 'refresh-token', + }) + expect(contentLength).toBe('5') + }) +}) + +describe('MSW harness — Copilot error shapes', () => { + it('reproduces isCopilotApiError 400 "Folder already exists"', async () => { + mockCopilot('/v1/files', () => copilotFolderExists()) + const client = copilotApi({ apiKey: 'k', token: 't' }) + try { + await client.listFiles({ channelId: 'ch' }) + expect.unreachable('listFiles should have thrown') + } catch (err) { + expect(isCopilotApiError(err)).toBe(true) + if (isCopilotApiError(err)) { + expect(err.status).toBe(400) + expect(err.body.message).toBe('Folder already exists') + } + } + }) + + it('reproduces isCopilotApiError 404', async () => { + mockCopilot('/v1/files', () => copilotNotFound()) + const client = copilotApi({ apiKey: 'k', token: 't' }) + try { + await client.listFiles({ channelId: 'ch' }) + expect.unreachable('listFiles should have thrown') + } catch (err) { + expect(isCopilotApiError(err)).toBe(true) + if (isCopilotApiError(err)) expect(err.status).toBe(404) + } + }) +}) + +describe('MSW harness — Dropbox error shapes', () => { + it('reproduces DropboxResponseError 409 with error.path not_found', async () => { + mockDropboxRpc('/2/files/get_metadata', () => dropboxGetMetadataNotFound()) + const dbx = new DropboxClient('refresh-token', null).getDropboxClient() + try { + await dbx.filesGetMetadata({ path: '/missing.txt' }) + expect.unreachable('filesGetMetadata should have thrown') + } catch (err) { + expect(err).toBeInstanceOf(DropboxResponseError) + const e = err as DropboxResponseError<{ error?: { path?: { '.tag'?: string } } }> + expect(e.status).toBe(409) + expect(e.error.error?.path?.['.tag']).toBe('not_found') + } + }) + + it('reproduces DropboxResponseError 409 with path_lookup/not_found summary', async () => { + mockDropboxRpc('/2/files/get_metadata', () => dropboxPathLookupNotFound()) + const dbx = new DropboxClient('refresh-token', null).getDropboxClient() + try { + await dbx.filesGetMetadata({ path: '/missing.txt' }) + expect.unreachable('filesGetMetadata should have thrown') + } catch (err) { + expect(err).toBeInstanceOf(DropboxResponseError) + const e = err as DropboxResponseError<{ error_summary?: string }> + expect(e.status).toBe(409) + expect(e.error.error_summary?.startsWith('path_lookup/not_found')).toBe(true) + } + }) +}) + +describe('MSW harness — pagination', () => { + it('traverses all Dropbox list_folder pages', async () => { + const entries = Array.from({ length: 250 }, (_, i) => ({ + '.tag': 'file', + id: `id:${i}`, + name: `f${i}.txt`, + path_lower: `/f${i}.txt`, + path_display: `/f${i}.txt`, + })) + server.use(...paginateDropboxListFolder(entries, { pageSize: 100 })) + const dbx = new DropboxClient('refresh-token', null) + const result = await dbx.getAllFilesFolders('/root', false, true) + expect(result).toHaveLength(250) + }) + + it('traverses all Copilot listFiles pages via nextToken', async () => { + const items = Array.from({ length: 250 }, (_, i) => ({ + id: `00000000-0000-4000-8000-${String(i).padStart(12, '0')}`, + channelId: 'ch_1', + name: `f${i}`, + object: 'file', + path: `/f${i}`, + })) + server.use(paginateCopilotListFiles(items, { pageSize: 100 })) + const api = new CopilotAPI('token') + const all: unknown[] = [] + let nextToken: string | undefined + do { + const pageResult = await api._listFiles('ch_1', nextToken) + all.push(...pageResult.data) + nextToken = pageResult.nextToken + } while (nextToken) + expect(all).toHaveLength(250) + }) +}) diff --git a/test/msw/overrides.ts b/test/msw/overrides.ts new file mode 100644 index 0000000..14fa056 --- /dev/null +++ b/test/msw/overrides.ts @@ -0,0 +1,25 @@ +import { type HttpResponseResolver, http } from 'msw' +import { COPILOT_HOST, DROPBOX_CONTENT_HOST, DROPBOX_RPC_HOST } from './hosts' +import { server } from './server' + +// Per-test overrides on the right host. They win over base handlers and are +// cleared by resetHandlers() in afterEach. +export function mockDropboxRpc(path: string, resolver: HttpResponseResolver): void { + server.use(http.post(`${DROPBOX_RPC_HOST}${path}`, resolver)) +} + +export function mockDropboxContent(path: string, resolver: HttpResponseResolver): void { + // Content endpoints (download/upload) POST via node-fetch. + server.use(http.post(`${DROPBOX_CONTENT_HOST}${path}`, resolver)) +} + +// Any msw HTTP verb — Copilot uses GET, DELETE, and PATCH/PUT. +type HttpMethod = keyof typeof http + +export function mockCopilot( + path: string, + resolver: HttpResponseResolver, + method: HttpMethod = 'get', +): void { + server.use(http[method](`${COPILOT_HOST}${path}`, resolver)) +} diff --git a/test/msw/pagination.ts b/test/msw/pagination.ts new file mode 100644 index 0000000..484de43 --- /dev/null +++ b/test/msw/pagination.ts @@ -0,0 +1,40 @@ +import { type HttpHandler, HttpResponse, http } from 'msw' +import { COPILOT_HOST, DROPBOX_RPC_HOST } from './hosts' + +// Each paginator tracks only the offset (from the cursor / nextToken), so one +// registration fakes one listing per test. Concurrent listings need separate ones. + +// Caller passes entries; this owns the cursor/has_more protocol. +export function paginateDropboxListFolder( + entries: unknown[], + { pageSize = 100 }: { pageSize?: number } = {}, +): HttpHandler[] { + const page = (offset: number) => { + const slice = entries.slice(offset, offset + pageSize) + const nextOffset = offset + slice.length + return { entries: slice, cursor: `cursor:${nextOffset}`, has_more: nextOffset < entries.length } + } + return [ + http.post(`${DROPBOX_RPC_HOST}/2/files/list_folder`, () => HttpResponse.json(page(0))), + http.post(`${DROPBOX_RPC_HOST}/2/files/list_folder/continue`, async ({ request }) => { + const { cursor } = (await request.json()) as { cursor: string } + const offset = Number(cursor.split(':')[1] ?? 0) + return HttpResponse.json(page(offset)) + }), + ] +} + +// Caller passes items; paginates via nextToken. +export function paginateCopilotListFiles( + items: unknown[], + { pageSize = 100 }: { pageSize?: number } = {}, +): HttpHandler { + return http.get(`${COPILOT_HOST}/v1/files`, ({ request }) => { + const token = new URL(request.url).searchParams.get('nextToken') + const offset = token ? Number(token.split(':')[1] ?? 0) : 0 + const slice = items.slice(offset, offset + pageSize) + const nextOffset = offset + slice.length + const nextToken = nextOffset < items.length ? `token:${nextOffset}` : undefined + return HttpResponse.json({ data: slice, ...(nextToken ? { nextToken } : {}) }) + }) +} diff --git a/test/msw/server.ts b/test/msw/server.ts new file mode 100644 index 0000000..b155c92 --- /dev/null +++ b/test/msw/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from 'msw/node' +import { copilotBaseHandlers } from './handlers/copilot' +import { dropboxBaseHandlers } from './handlers/dropbox' + +export const server = setupServer(...dropboxBaseHandlers, ...copilotBaseHandlers) diff --git a/test/support/placeholder-env.ts b/test/support/placeholder-env.ts index 056c76b..7966eb1 100644 --- a/test/support/placeholder-env.ts +++ b/test/support/placeholder-env.ts @@ -1,13 +1,16 @@ -// Placeholder server env so Zod-validated `server-only` modules import cleanly in -// tests. Values are never asserted on — they only satisfy `ServerEnvSchema` -// (src/config/server.env.ts). DATABASE_URL is intentionally NOT set here: the -// unit run stubs it, the integration run points it at the Testcontainers DB. +// Placeholder server env so Zod-validated `server-only` modules import in tests. +// Most values just satisfy `ServerEnvSchema`. Two affect behaviour: COPILOT_ENV +// (Copilot SDK apiKey-trust) and DROPBOX_API_URL (the MSW content host). +// DATABASE_URL is set per-run elsewhere (unit stubs it; integration uses Testcontainers). export function applyPlaceholderServerEnv() { + // Copilot SDK trusts the supplied apiKey (skips token decryption) only when + // COPILOT_ENV is 'local'/'__SECRET_STAGING__'. Vitest doesn't load `.env`. + process.env.COPILOT_ENV ??= 'local' process.env.COPILOT_API_KEY ??= 'test-copilot-api-key' process.env.DROPBOX_APP_KEY ??= 'test-dropbox-app-key' process.env.DROPBOX_APP_SECRET ??= 'test-dropbox-app-secret' process.env.DROPBOX_REDIRECT_URI ??= 'https://test.example.com/callback' process.env.DROPBOX_SCOPES ??= 'files.content.read files.content.write' - process.env.DROPBOX_API_URL ??= 'https://api.dropboxapi.com' + process.env.DROPBOX_API_URL ??= 'https://content.dropboxapi.com' process.env.WEBHOOK_CATCHUP_CRON ??= '*/5 * * * *' } diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts index ce73346..2b1b388 100644 --- a/vitest.integration.config.ts +++ b/vitest.integration.config.ts @@ -15,9 +15,17 @@ export default defineConfig({ }, test: { environment: 'node', - include: ['src/**/*.integration.test.ts'], + include: ['src/**/*.integration.test.ts', 'test/**/*.integration.test.ts'], globalSetup: ['./test/integration/global-setup.ts'], setupFiles: ['./test/integration/setup.ts'], + server: { + // `copilot-node-sdk` is a pure-ESM package whose dist does an internal + // directory import (`../codegen/api`, resolved to its `index.js` only by + // bundler-style resolution). Node's native ESM loader can't resolve that + // when Vite externalizes the package, so force it through Vite's own + // resolver instead. + deps: { inline: ['copilot-node-sdk'] }, + }, // One container, one shared DB — run files serially so truncate-between-tests // isolation is safe. fileParallelism: false, From d9f72f67339dfc5085965a825ac87fd77865fc4b Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 15 Jul 2026 18:05:05 +0545 Subject: [PATCH 3/5] chore(OUT-3986): forbid production code importing test files (Biome) Add a scoped noRestrictedImports rule: files under src/ (excluding co-located tests) may not import the test harness or test files (test/**, **/__tests__/**, *.test.*). Keeps test-only code out of the production bundle. Test files are exempt and keep full import freedom. Co-Authored-By: Claude Opus 4.8 --- biome.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index ca22494..8c6f6e6 100644 --- a/biome.json +++ b/biome.json @@ -64,5 +64,33 @@ "semicolons": "asNeeded", "quoteStyle": "single" } - } + }, + "overrides": [ + { + "includes": ["src/**", "!**/__tests__/**", "!**/*.test.ts", "!**/*.test.tsx"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "**/__tests__/**", + "**/test/**", + "test/**", + "**/*.test", + "**/*.test.*" + ], + "message": "Production code must not import test files or the test harness (test/**, **/__tests__/**, *.test.*)." + } + ] + } + } + } + } + } + } + ] } From 87d0a94a22b96b3e203c16b4b694631cec1462a9 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 15 Jul 2026 20:19:09 +0545 Subject: [PATCH 4/5] fix(OUT-3986): reject non-positive pageSize in pagination helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pageSize of 0 (or negative / non-integer) made slice return no entries, so nextOffset never advanced while has_more stayed true — a consumer following the cursor/nextToken would loop on the same page until timeout. Both paginators now fail fast with a clear error, and a guard test covers it. Co-Authored-By: Claude Opus 4.8 --- test/msw/msw-harness.integration.test.ts | 5 +++++ test/msw/pagination.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/test/msw/msw-harness.integration.test.ts b/test/msw/msw-harness.integration.test.ts index 6dbe26e..7d09cc2 100644 --- a/test/msw/msw-harness.integration.test.ts +++ b/test/msw/msw-harness.integration.test.ts @@ -146,4 +146,9 @@ describe('MSW harness — pagination', () => { } while (nextToken) expect(all).toHaveLength(250) }) + + it('rejects a non-positive pageSize instead of producing a non-progressing paginator', () => { + expect(() => paginateDropboxListFolder([1], { pageSize: 0 })).toThrow(/positive integer/) + expect(() => paginateCopilotListFiles([1], { pageSize: 0 })).toThrow(/positive integer/) + }) }) diff --git a/test/msw/pagination.ts b/test/msw/pagination.ts index 484de43..2cb7dd3 100644 --- a/test/msw/pagination.ts +++ b/test/msw/pagination.ts @@ -4,11 +4,20 @@ import { COPILOT_HOST, DROPBOX_RPC_HOST } from './hosts' // Each paginator tracks only the offset (from the cursor / nextToken), so one // registration fakes one listing per test. Concurrent listings need separate ones. +// A non-positive (or non-integer) pageSize yields empty pages that never advance +// the offset while has_more stays true — a consumer would loop forever. Fail fast. +function assertPositivePageSize(fn: string, pageSize: number): void { + if (!Number.isInteger(pageSize) || pageSize < 1) { + throw new Error(`${fn}: pageSize must be a positive integer, got ${pageSize}`) + } +} + // Caller passes entries; this owns the cursor/has_more protocol. export function paginateDropboxListFolder( entries: unknown[], { pageSize = 100 }: { pageSize?: number } = {}, ): HttpHandler[] { + assertPositivePageSize('paginateDropboxListFolder', pageSize) const page = (offset: number) => { const slice = entries.slice(offset, offset + pageSize) const nextOffset = offset + slice.length @@ -29,6 +38,7 @@ export function paginateCopilotListFiles( items: unknown[], { pageSize = 100 }: { pageSize?: number } = {}, ): HttpHandler { + assertPositivePageSize('paginateCopilotListFiles', pageSize) return http.get(`${COPILOT_HOST}/v1/files`, ({ request }) => { const token = new URL(request.url).searchParams.get('nextToken') const offset = token ? Number(token.split(':')[1] ?? 0) : 0 From a4e21778b7c7497edfeafd66e6fd4902ce14f629 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 15 Jul 2026 20:30:13 +0545 Subject: [PATCH 5/5] fix(OUT-3986): treat *.spec files as tests in the import-boundary rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit config recognizes src/**/*.spec.ts as tests, but the noRestrictedImports override only exempted *.test.ts/tsx — so a co-located *.spec file importing a test/ helper was wrongly flagged as production. Exempt *.spec.ts/tsx on the importer side and add *.spec.* to the banned import targets, so .spec and .test are handled uniformly. Co-Authored-By: Claude Opus 4.8 --- biome.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/biome.json b/biome.json index 8c6f6e6..d6c9286 100644 --- a/biome.json +++ b/biome.json @@ -67,7 +67,14 @@ }, "overrides": [ { - "includes": ["src/**", "!**/__tests__/**", "!**/*.test.ts", "!**/*.test.tsx"], + "includes": [ + "src/**", + "!**/__tests__/**", + "!**/*.test.ts", + "!**/*.test.tsx", + "!**/*.spec.ts", + "!**/*.spec.tsx" + ], "linter": { "rules": { "style": { @@ -81,9 +88,11 @@ "**/test/**", "test/**", "**/*.test", - "**/*.test.*" + "**/*.test.*", + "**/*.spec", + "**/*.spec.*" ], - "message": "Production code must not import test files or the test harness (test/**, **/__tests__/**, *.test.*)." + "message": "Production code must not import test files or the test harness (test/**, **/__tests__/**, *.test.*, *.spec.*)." } ] }