Repository: idosal/git-mcp
Pull request #126: tests: add E2E infra
Author: login: idosal
Created: 2025-04-24T23:43:55Z | Merged: 2025-04-26T20:51:32Z



Diff:
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
new file mode 100644
index 00000000..228d15bf
--- /dev/null
+++ b/.github/workflows/e2e-tests.yml
@@ -0,0 +1,56 @@
+name: E2E Tests
+
+on:
+  pull_request:
+    paths:
+      - 'src/**'
+      - 'tests/e2e/**'
+      - 'package.json'
+      - 'pnpm-lock.yaml'
+      - 'wrangler.jsonc'
+      - '.github/workflows/e2e-tests.yml'
+
+jobs:
+  e2e_test:
+    runs-on: ubuntu-latest
+    timeout-minutes: 15 # Adjust timeout as needed
+    permissions:
+      contents: read
+      pull-requests: write # Required to post the preview URL comment back to the PR
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20' # Match your project's Node.js version requirement
+
+      - name: Setup PNPM
+        uses: pnpm/action-setup@v4
+
+      - name: Get pnpm store directory
+        shell: bash
+        run: |
+          echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+
+      - name: Setup pnpm cache
+        uses: actions/cache@v4
+        with:
+          path: ${{ env.STORE_PATH }}
+          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+          restore-keys: |
+            ${{ runner.os }}-pnpm-store-
+
+      - name: Install dependencies
+        run: pnpm install --frozen-lockfile
+
+      - name: Install Playwright Browsers
+        run: npx playwright install chromium --with-deps
+
+      - name: Create .dev.vars with test environment
+        run: echo "ENVIRONMENT=test" > .dev.vars
+
+      - name: Run Playwright tests
+        run: pnpm run test:e2e
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1bb723ed..b54aa853 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,3 +177,9 @@ dist
 
 .dev.vars
 .wrangler/
+
+# playwright
+
+playwright-results/
+playwright-report/
+test-results/
\ No newline at end of file
diff --git a/package.json b/package.json
index 2b93abad..f8279acc 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "lint:fix": "biome lint --fix",
     "cf-typegen": "wrangler types",
     "test": "vitest run",
+    "test:e2e": "playwright test",
     "prepare": "husky"
   },
   "lint-staged": {
@@ -86,15 +87,18 @@
   "devDependencies": {
     "@cloudflare/vite-plugin": "^0.1.21",
     "@cloudflare/workers-types": "^4.20250418.0",
+    "@playwright/test": "^1.52.0",
     "@react-router/dev": "^7.5.1",
     "@tailwindcss/postcss": "^4.1.4",
     "@tailwindcss/vite": "^4.1.4",
     "@types/node": "^20.17.30",
+    "@types/node-fetch": "^2.6.12",
     "@types/react": "^19.0.1",
     "@types/react-dom": "^19.0.1",
     "husky": "^9.1.7",
     "lint-staged": "^15.5.1",
     "marked": "^15.0.8",
+    "node-fetch": "^3.3.2",
     "prettier": "^3.5.3",
     "tailwindcss": "^4.1.4",
     "typescript": "^5.8.3",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..99621aec
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,77 @@
+import { defineConfig, devices } from '@playwright/test';
+import path from 'path';
+import { fileURLToPath } from 'url'; // Import necessary functions
+
+// ES Module equivalent of __dirname
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: './tests/e2e', // Point to your end-to-end tests directory
+
+  // Path to the global setup file.
+  // Use the derived __dirname for correct path resolution in ESM
+  globalSetup: path.resolve(__dirname, './tests/global-setup.ts'),
+
+  /* Run tests in files in parallel */
+  fullyParallel: true,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 3 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: 'html',
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: 'http://localhost:5174', // Point to the local Inspector UI
+
+    /* Run tests in headed mode */
+    headless: process.env.CI ? true : false,
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'on-first-retry',
+  },
+
+  /* Configure projects for major browsers */
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+
+    // {
+    //   name: 'firefox',
+    //   use: { ...devices['Desktop Firefox'] },
+    // },
+
+    // {
+    //   name: 'webkit',
+    //   use: { ...devices['Desktop Safari'] },
+    // },
+  ],
+
+  /* Configure multiple web servers */
+  webServer: [
+    {
+      name: 'GitMCP-DevServer',
+      command: 'pnpm run dev',
+      url: 'http://localhost:5173',
+      stderr: 'pipe',
+      timeout: 120 * 1000,
+    },
+    {
+      name: 'MCP Inspector',
+      command: 'CLIENT_PORT=5174 SERVER_PORT=6277 npx @modelcontextprotocol/inspector',
+      url: 'http://localhost:5174', // Inspector UI URL (matches baseURL)
+      stdout: 'ignore',
+      stderr: 'pipe',
+      timeout: 120 * 1000, // Give inspector time to start
+    }
+  ],
+}); 
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b5059a05..73152d07 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -156,6 +156,9 @@ importers:
       '@cloudflare/workers-types':
         specifier: ^4.20250418.0
         version: 4.20250418.0
+      '@playwright/test':
+        specifier: ^1.52.0
+        version: 1.52.0
       '@react-router/dev':
         specifier: ^7.5.1
         version: 7.5.1(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(react-router@7.5.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.19.3)(typescript@5.8.3)(vite@6.3.1(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.3)(yaml@2.7.1))(wrangler@4.12.0(@cloudflare/workers-types@4.20250418.0))(yaml@2.7.1)
@@ -168,6 +171,9 @@ importers:
       '@types/node':
         specifier: ^20.17.30
         version: 20.17.30
+      '@types/node-fetch':
+        specifier: ^2.6.12
+        version: 2.6.12
       husky:
         specifier: ^9.1.7
         version: 9.1.7
@@ -177,6 +183,9 @@ importers:
       marked:
         specifier: ^15.0.8
         version: 15.0.8
+      node-fetch:
+        specifier: ^3.3.2
+        version: 3.3.2
       prettier:
         specifier: ^3.5.3
         version: 3.5.3
@@ -1015,6 +1024,11 @@ packages:
     resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
     engines: {node: '>=14'}
 
+  '@playwright/test@1.52.0':
+    resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   '@radix-ui/number@1.1.1':
     resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
 
@@ -1738,6 +1752,9 @@ packages:
   '@types/ms@2.1.0':
     resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
 
+  '@types/node-fetch@2.6.12':
+    resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
+
   '@types/node@20.17.30':
     resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==}
 
@@ -1863,6 +1880,9 @@ packages:
   assertion-error@1.1.0:
     resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
 
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
   babel-dead-code-elimination@1.0.10:
     resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==}
 
@@ -1995,6 +2015,10 @@ packages:
   colorette@2.0.20:
     resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
 
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
   comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
 
@@ -2053,6 +2077,10 @@ packages:
   data-uri-to-buffer@2.0.2:
     resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
 
+  data-uri-to-buffer@4.0.1:
+    resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+    engines: {node: '>= 12'}
+
   debug@4.4.0:
     resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
     engines: {node: '>=6.0'}
@@ -2080,6 +2108,10 @@ packages:
   defu@6.1.4:
     resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
 
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
   depd@2.0.0:
     resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
     engines: {node: '>= 0.8'}
@@ -2165,6 +2197,10 @@ packages:
     resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
     engines: {node: '>= 0.4'}
 
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
   esbuild@0.21.5:
     resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
     engines: {node: '>=12'}
@@ -2262,6 +2298,10 @@ packages:
       picomatch:
         optional: true
 
+  fetch-blob@3.2.0:
+    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+    engines: {node: ^12.20 || >= 14.13}
+
   file-set@5.2.2:
     resolution: {integrity: sha512-/KgJI1V/QaDK4enOk/E2xMFk1cTWJghEr7UmWiRZfZ6upt6gQCfMn4jJ7aOm64OKurj4TaVnSSgSDqv5ZKYA3A==}
     engines: {node: '>=12.17'}
@@ -2283,6 +2323,14 @@ packages:
     resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
     engines: {node: '>=14'}
 
+  form-data@4.0.2:
+    resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
+    engines: {node: '>= 6'}
+
+  formdata-polyfill@4.0.10:
+    resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+    engines: {node: '>=12.20.0'}
+
   forwarded@0.2.0:
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     engines: {node: '>= 0.6'}
@@ -2313,6 +2361,11 @@ packages:
     resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
     engines: {node: '>=14.14'}
 
+  fsevents@2.3.2:
+    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
   fsevents@2.3.3:
     resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2387,6 +2440,10 @@ packages:
     resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
     engines: {node: '>= 0.4'}
 
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
   hasown@2.0.2:
     resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
     engines: {node: '>= 0.4'}
@@ -2967,9 +3024,18 @@ packages:
       react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
       react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
 
+  node-domexception@1.0.0:
+    resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+    engines: {node: '>=10.5.0'}
+    deprecated: Use your platform's native DOMException instead
+
   node-fetch-native@1.6.6:
     resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
 
+  node-fetch@3.3.2:
+    resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
   node-releases@2.0.19:
     resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
 
@@ -3113,6 +3179,16 @@ packages:
   pkg-types@1.3.1:
     resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
 
+  playwright-core@1.52.0:
+    resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  playwright@1.52.0:
+    resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   postcss@8.5.3:
     resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
     engines: {node: ^10 || ^12 || >=14}
@@ -3826,6 +3902,10 @@ packages:
     resolution: {integrity: sha512-e/FRLDVdZQWFrAzU6Hdvpm7D7m2ina833gIKLptQykRK49mmCYHLHq7UqjPDbxbKLZkTkW1rFqbengdE3sLfdw==}
     engines: {node: '>=12.17'}
 
+  web-streams-polyfill@3.3.3:
+    resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+    engines: {node: '>= 8'}
+
   which@1.3.1:
     resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
     hasBin: true
@@ -4651,6 +4731,10 @@ snapshots:
   '@pkgjs/parseargs@0.11.0':
     optional: true
 
+  '@playwright/test@1.52.0':
+    dependencies:
+      playwright: 1.52.0
+
   '@radix-ui/number@1.1.1': {}
 
   '@radix-ui/primitive@1.1.2': {}
@@ -5349,6 +5433,11 @@ snapshots:
 
   '@types/ms@2.1.0': {}
 
+  '@types/node-fetch@2.6.12':
+    dependencies:
+      '@types/node': 20.17.30
+      form-data: 4.0.2
+
   '@types/node@20.17.30':
     dependencies:
       undici-types: 6.19.8
@@ -5480,6 +5569,8 @@ snapshots:
 
   assertion-error@1.1.0: {}
 
+  asynckit@0.4.0: {}
+
   babel-dead-code-elimination@1.0.10:
     dependencies:
       '@babel/core': 7.26.10
@@ -5621,6 +5712,10 @@ snapshots:
 
   colorette@2.0.20: {}
 
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
   comma-separated-tokens@2.0.3: {}
 
   commander@13.1.0: {}
@@ -5662,6 +5757,8 @@ snapshots:
 
   data-uri-to-buffer@2.0.2: {}
 
+  data-uri-to-buffer@4.0.1: {}
+
   debug@4.4.0:
     dependencies:
       ms: 2.1.3
@@ -5678,6 +5775,8 @@ snapshots:
 
   defu@6.1.4: {}
 
+  delayed-stream@1.0.0: {}
+
   depd@2.0.0: {}
 
   dequal@2.0.3: {}
@@ -5737,6 +5836,13 @@ snapshots:
     dependencies:
       es-errors: 1.3.0
 
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
   esbuild@0.21.5:
     optionalDependencies:
       '@esbuild/aix-ppc64': 0.21.5
@@ -5895,6 +6001,11 @@ snapshots:
     optionalDependencies:
       picomatch: 4.0.2
 
+  fetch-blob@3.2.0:
+    dependencies:
+      node-domexception: 1.0.0
+      web-streams-polyfill: 3.3.3
+
   file-set@5.2.2:
     dependencies:
       array-back: 6.2.2
@@ -5920,6 +6031,17 @@ snapshots:
       cross-spawn: 7.0.6
       signal-exit: 4.1.0
 
+  form-data@4.0.2:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      mime-types: 2.1.35
+
+  formdata-polyfill@4.0.10:
+    dependencies:
+      fetch-blob: 3.2.0
+
   forwarded@0.2.0: {}
 
   framer-motion@12.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -5945,6 +6067,9 @@ snapshots:
       jsonfile: 6.1.0
       universalify: 2.0.1
 
+  fsevents@2.3.2:
+    optional: true
+
   fsevents@2.3.3:
     optional: true
 
@@ -6014,6 +6139,10 @@ snapshots:
 
   has-symbols@1.1.0: {}
 
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
   hasown@2.0.2:
     dependencies:
       function-bind: 1.1.2
@@ -6785,8 +6914,16 @@ snapshots:
       react: 19.1.0
       react-dom: 19.1.0(react@19.1.0)
 
+  node-domexception@1.0.0: {}
+
   node-fetch-native@1.6.6: {}
 
+  node-fetch@3.3.2:
+    dependencies:
+      data-uri-to-buffer: 4.0.1
+      fetch-blob: 3.2.0
+      formdata-polyfill: 4.0.10
+
   node-releases@2.0.19: {}
 
   normalize-package-data@5.0.0:
@@ -6918,6 +7055,14 @@ snapshots:
       mlly: 1.7.4
       pathe: 2.0.3
 
+  playwright-core@1.52.0: {}
+
+  playwright@1.52.0:
+    dependencies:
+      playwright-core: 1.52.0
+    optionalDependencies:
+      fsevents: 2.3.2
+
   postcss@8.5.3:
     dependencies:
       nanoid: 3.3.11
@@ -7670,6 +7815,8 @@ snapshots:
 
   walk-back@5.1.1: {}
 
+  web-streams-polyfill@3.3.3: {}
+
   which@1.3.1:
     dependencies:
       isexe: 2.0.0
diff --git a/src/api/test-setup.ts b/src/api/test-setup.ts
new file mode 100644
index 00000000..928d7cd4
--- /dev/null
+++ b/src/api/test-setup.ts
@@ -0,0 +1,85 @@
+export interface TestSetupBindings extends Env {
+  DOCS_BUCKET: R2Bucket; // Ensure this matches CloudflareEnvironment definition
+  ENVIRONMENT?: string; // Environment variable to check if we are in 'test' mode
+}
+
+const dummyFilesToUpload = [
+  {
+    bucketPath: "remix-run/react-router/llms.txt",
+    content:
+      "# React Router Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bucketPath: "answerdotai/fasthtml/llms.txt",
+    content:
+      "# FastHTML Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bucketPath: "mrdoob/three.js/llms.txt",
+    content:
+      "# Three.js Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+];
+
+export async function handleR2TestSetup(
+  env: TestSetupBindings,
+): Promise<Response> {
+  if (env.ENVIRONMENT !== "test") {
+    console.warn("Attempted to run R2 setup outside test environment.");
+    return new Response("Forbidden", {
+      status: 403,
+      headers: { "Content-Type": "application/json" },
+    });
+  }
+
+  console.log("Handling R2 setup for tests using hardcoded content...");
+
+  if (!env.DOCS_BUCKET) {
+    console.error("DOCS_BUCKET binding not found in environment.");
+    return new Response(
+      JSON.stringify({ error: "R2 bucket binding not configured" }),
+      {
+        status: 500,
+        headers: { "Content-Type": "application/json" },
+      },
+    );
+  }
+
+  const results = [];
+  let errors = 0;
+
+  for (const file of dummyFilesToUpload) {
+    try {
+      await env.DOCS_BUCKET.put(file.bucketPath, file.content);
+      results.push(
+        `Successfully uploaded hardcoded content to ${file.bucketPath}`,
+      );
+      console.log(`Uploaded hardcoded content to ${file.bucketPath} in R2`);
+    } catch (error) {
+      const errorMessage =
+        error instanceof Error ? error.message : String(error);
+      console.error(
+        `Error uploading hardcoded content to ${file.bucketPath} in R2:`,
+        errorMessage,
+      );
+      results.push(
+        `Failed to upload hardcoded content to ${file.bucketPath}: ${errorMessage}`,
+      );
+      errors++;
+    }
+  }
+
+  const responseBody = {
+    message:
+      errors > 0
+        ? `R2 setup partially failed. ${errors} errors.`
+        : "R2 setup with hardcoded content successful.",
+    details: results,
+  };
+  const status = errors > 0 ? 500 : 200;
+
+  return new Response(JSON.stringify(responseBody), {
+    status: status,
+    headers: { "Content-Type": "application/json" },
+  });
+}
diff --git a/src/api/tools/commonTools.ts b/src/api/tools/commonTools.ts
index 3654fa2d..cabb2d57 100644
--- a/src/api/tools/commonTools.ts
+++ b/src/api/tools/commonTools.ts
@@ -205,7 +205,7 @@ export async function fetchDocumentation({
         console.log(`Fetched pre-generated llms.txt for ${owner}/${repo}`);
         fileUsed = "llms.txt (generated)";
       } else {
-        console.log(`No pre-generated llms.txt found for ${owner}/${repo}`);
+        console.error(`No pre-generated llms.txt found for ${owner}/${repo}`);
       }
     }
 
diff --git a/src/index.ts b/src/index.ts
index 4a9ef5a1..709ff083 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ import {
   withViewTracking,
 } from "./api/utils/badge";
 import { getRepoData } from "./shared/repoData";
+import { handleR2TestSetup } from "./api/test-setup";
 
 export { ViewCounterDO } from "./api/utils/ViewCounterDO";
 
@@ -41,7 +42,7 @@ const handleCorsPreflightRequest = (): Response => {
     headers: {
       "Access-Control-Allow-Origin": "*",
       "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
-      "Access-Control-Allow-Headers": "Content-Type",
+      "Access-Control-Allow-Headers": "*",
       "Access-Control-Allow-Credentials": "true",
       "Access-Control-Max-Age": "86400", // 24 hours
     },
@@ -86,23 +87,19 @@ export class MyMCP extends McpAgent {
     url.hash = "";
     const canonicalUrl = url.toString();
 
-    // Access env from this.env (Cloudflare worker environment is accessible here)
     const env = this.env as CloudflareEnvironment;
     const ctx = this.ctx;
 
-    // Get the repository data from the URL
     const repoData = getRepoData({
       requestHost: host,
       requestUrl: canonicalUrl,
     });
 
-    // Pass env to getMcpTools
     getMcpTools(env, host, canonicalUrl, ctx).forEach((tool) => {
       this.server.tool(
         tool.name,
         tool.description,
         tool.paramsSchema,
-        // Wrap the callback with view tracking
         withViewTracking(env, ctx, repoData, async (args: any) => {
           return tool.cb(args);
         }),
@@ -116,17 +113,24 @@ const mcpHandler = MyMCP.mount("/*");
 // Export a request handler that checks the transport header
 export default {
   async fetch(request: Request, env: any, ctx: any) {
+    const url = new URL(request.url);
+    const pathname = url.pathname;
+
+    if (
+      env.ENVIRONMENT === "test" &&
+      request.method === "POST" &&
+      pathname === "/api/setup-r2-for-tests"
+    ) {
+      return await handleR2TestSetup(env as CloudflareEnvironment);
+    }
+
     // Handle CORS preflight requests
     if (request.method === "OPTIONS") {
       return handleCorsPreflightRequest();
     }
 
-    const url = new URL(request.url);
-    const pathname = url.pathname;
-
     // Handle badge requests
     if (pathname.startsWith("/badge/")) {
-      // Extract owner and repo from URL pattern: /badge/:owner/:repo
       const parts = pathname.split("/").filter(Boolean);
       if (parts.length >= 3 && parts[0] === "badge") {
         const owner = parts[1];
@@ -143,6 +147,7 @@ export default {
       request.method === "POST" &&
       url.pathname.includes("/message") &&
       url.pathname !== "/message";
+
     ctx.props.request = request;
 
     if (isMessage) {
@@ -154,12 +159,7 @@ export default {
       if (!newHeaders.has("accept")) {
         newHeaders.set("Content-Type", "text/event-stream");
       }
-
-      const modifiedRequest = new Request(request, {
-        headers: newHeaders,
-      });
-
-      // Handle SSE request with MCP handler
+      const modifiedRequest = new Request(request, { headers: newHeaders });
       return await mcpHandler.fetch(modifiedRequest, env, ctx);
     } else {
       // Default to serving the regular page
diff --git a/tests/e2e/inspection.spec.ts b/tests/e2e/inspection.spec.ts
new file mode 100644
index 00000000..2c6b5f76
--- /dev/null
+++ b/tests/e2e/inspection.spec.ts
@@ -0,0 +1,107 @@
+import { test, expect } from "@playwright/test";
+
+// Define the base target dev server URL (the one the Inspector connects to)
+const targetServerBaseUrl = "http://localhost:5173"; // Base URL without path or /sse
+
+// Define test cases with specific paths and expectations
+const testCases = [
+  {
+    path: "mrdoob/three.js",
+    // Expectations removed as the specific fetch test will be skipped for this path
+  },
+  {
+    path: "remix-run/react-router",
+    expectedContentSnippet: "# React Router"
+  },
+  {
+    path: "answerdotai/fasthtml",
+    expectedContentSnippet: "# FastHTML"
+  }
+];
+
+test.describe("Dedicated repo servers", () => {
+  test.describe("SSE", () => {
+    // Loop through the defined test cases
+    for (const testCase of testCases) {
+      const { path, expectedContentSnippet } = testCase;
+      const targetServerUrl = `${targetServerBaseUrl}/${path}`; // Construct full URL for connection
+
+      // Test to verify listing tools (this should still run for all)
+      test(`should list tools for ${path}`, async ({ page }) => {
+        await page.goto("/");
+        await page.getByRole("combobox", { name: "Transport Type" }).click();
+        await page.getByRole("option", { name: "SSE" }).click();
+        await page.getByRole("textbox", { name: "URL" }).fill(targetServerUrl);
+        await page.getByRole("button", { name: "Connect" }).click();
+
+        await expect(page.getByRole("button", { name: "List Tools" })).toBeVisible({ timeout: 5000 });
+        await page.getByRole("button", { name: "List Tools" }).click();
+
+        await expect(page.getByText("fetch_generic_url_content")).toBeVisible({
+          timeout: 10000,
+        });
+        if (path === 'mrdoob/three.js') {
+          //  await expect(page.getByText('fetch_threejs_documentation')).toBeVisible({ timeout: 5000 });
+           // Check for the specific search tool for three.js
+           await expect(page.getByText('search_threejs_documentation')).toBeVisible({ timeout: 5000 });
+        } else {
+           // Check for generic fetch and search tools
+           await expect(page.getByText(/^fetch_.*_documentation$/)).toBeVisible({ timeout: 5000 });
+           await expect(page.getByText(/^search_.*_documentation$/)).toBeVisible({ timeout: 5000 });
+        }
+      });
+
+      // Test to verify running the generic fetch tool (this should still run for all)
+      test(`should fetch documentation using generic tool for ${path}`, async ({ page }) => {
+        await page.goto("/");
+        await page.getByRole("combobox", { name: "Transport Type" }).click();
+        await page.getByRole("option", { name: "SSE" }).click();
+        await page.getByRole("textbox", { name: "URL" }).fill(targetServerUrl);
+        await page.getByRole("button", { name: "Connect" }).click();
+
+        await expect(page.getByRole("button", { name: "List Tools" })).toBeVisible({ timeout: 5000 });
+        await page.getByRole("button", { name: "List Tools" }).click();
+
+        await expect(page.getByText("fetch_generic_url_content")).toBeVisible({ timeout: 5000 });
+        await page.getByText('fetch_generic_url_content').click();
+        await page.getByRole('textbox', { name: 'url', exact: true  }).fill('https://www.makeareadme.com/');
+        await page.getByRole('button', { name: 'Run Tool' }).click();
+
+        await expect(page.getByText('Success')).toBeVisible({ timeout: 10000 });
+        await expect(page.getByText('# Make a README')).toBeVisible();
+      });
+
+      // Test to verify running the specific fetch_X_documentation tool
+      // Conditionally skip this test for mrdoob/three.js
+      const shouldSkipFetchTest = path === 'mrdoob/three.js';
+      test(`should run fetch_X_documentation for ${path}`, async ({ page }) => {
+         test.skip(shouldSkipFetchTest, 'Skipping fetch_X_documentation test for mrdoob/three.js as it uses a different tool name.');
+
+         await page.goto("/");
+         await page.getByRole("combobox", { name: "Transport Type" }).click();
+         await page.getByRole("option", { name: "SSE" }).click();
+         await page.getByRole("textbox", { name: "URL" }).fill(targetServerUrl);
+         await page.getByRole("button", { name: "Connect" }).click();
+
+         await expect(page.getByRole("button", { name: "List Tools" })).toBeVisible({ timeout: 5000 });
+         await page.getByRole("button", { name: "List Tools" }).click();
+
+         const fetchToolButton = page.getByText(/^fetch_.*_documentation$/);
+         await expect(fetchToolButton).toBeVisible({ timeout: 5000 });
+         await fetchToolButton.click();
+
+         await page.getByRole('button', { name: 'Run Tool' }).click();
+
+         await expect(page.getByText('Success')).toBeVisible({ timeout: 15000 });
+
+         // Use the expectedContentSnippet from the test case
+         // Ensure expectedContentSnippet is defined before using it
+         if (expectedContentSnippet) {
+             await expect(page.getByText(expectedContentSnippet)).toBeVisible();
+         } else {
+             console.warn(`No expectedContentSnippet defined for path: ${path}`);
+         }
+      });
+    }
+  });
+});
diff --git a/tests/global-setup.ts b/tests/global-setup.ts
new file mode 100644
index 00000000..d9c1fe27
--- /dev/null
+++ b/tests/global-setup.ts
@@ -0,0 +1,55 @@
+import { FullConfig } from '@playwright/test';
+import fetch from 'node-fetch';
+
+// URL of your running worker during tests
+const workerUrl = 'http://localhost:5173'; 
+const setupEndpoint = `${workerUrl}/api/setup-r2-for-tests`;
+
+async function globalSetup(config: FullConfig) {
+  console.log('--- Starting Global Setup: Populating R2 via Endpoint ---');
+
+  // Add a delay to allow the web server to fully start
+  // Playwright's webServer.url check confirms availability, but sometimes 
+  // internal initialization might still be happening.
+  console.log('Waiting 3 seconds for server to stabilize...');
+  await new Promise(resolve => setTimeout(resolve, 3000)); 
+
+  try {
+    console.log(`Sending POST request to ${setupEndpoint}...`);
+    const response = await fetch(setupEndpoint, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        // Add any other headers required by your endpoint if necessary
+      },
+      // No body needed as the endpoint knows what to do
+    });
+
+    const responseBodyText = await response.text(); // Read body once
+    console.log(`Global Setup: Received response status ${response.status}`);
+    console.log(`Global Setup: Received response body: ${responseBodyText}`);
+
+
+    if (!response.ok) {
+      throw new Error(`Failed to setup R2 via endpoint: ${response.status} ${response.statusText} - Body: ${responseBodyText}`);
+    }
+
+    // Try parsing JSON only if response is ok
+    try {
+        const result = JSON.parse(responseBodyText); 
+        console.log('Global Setup: R2 Population Result:', result.message);
+    } catch (parseError) {
+        console.error("Global Setup: Failed to parse JSON response from setup endpoint.");
+        throw new Error(`Failed to parse JSON response: ${parseError} - Original Body: ${responseBodyText}`);
+    }
+    
+    console.log('--- Global Setup Finished Successfully ---');
+
+  } catch (error) {
+    console.error('Error during global setup (calling R2 population endpoint):', error);
+    // Throw error to stop test execution if setup fails critically
+    throw new Error(`Global setup failed: ${error}`);
+  }
+}
+
+export default globalSetup; 
\ No newline at end of file
diff --git a/vitest.config.ts b/vitest.config.ts
index a2587b9d..1e87d8fd 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -2,6 +2,6 @@ import { configDefaults, defineConfig } from "vitest/config";
 
 export default defineConfig({
   test: {
-    exclude: [...configDefaults.exclude, "build/**/*", "dist/**/*"],
+    exclude: [...configDefaults.exclude, "build/**/*", "dist/**/*", "tests/e2e/**/*"],
   },
 });
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 0b1555b7..bd5ac0d0 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -7,7 +7,7 @@
   "name": "git-mcp",
   "main": "src/index.ts",
   "compatibility_flags": ["nodejs_compat"],
-  "compatibility_date": "2025-04-17",
+  "compatibility_date": "2025-04-26",
   "routes": [
     {
       "pattern": "gitmcp.io",


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces new end-to-end testing infrastructure using GitHub Actions for pull requests.  
- Adds a new workflow file for running E2E tests on pull requests  
- Includes steps for checkout, setting up Node.js and PNPM, dependency installation, building the app, deploying a preview via Cloudflare, and running Playwright tests

<details>
<summary>Files not reviewed (1)</summary>

* **wrangler.jsonc**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T23:44:04Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds end-to-end testing infrastructure, including a global setup to populate R2 storage via an API endpoint and multiple E2E test specs to verify various repository functionalities. Key changes include:
- Introduction of new test files and configurations (global-setup, inspection.spec) for E2E tests.
- Updates to the main index for handling test-specific API routes.
- Enhancements to logging and a new test setup module for configuring R2 with hardcoded file uploads.
- Updates to Playwright and workflow configurations to support E2E testing.

### Reviewed Changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                    | Description                                                       |
| --------------------------------------- | ----------------------------------------------------------------- |
| tests/global-setup.ts                   | New global setup file to trigger R2 population before tests run.  |
| tests/e2e/inspection.spec.ts            | New E2E test specs verifying repo server tools functionality.     |
| src/index.ts                            | Updated request handler to support test-specific R2 setup endpoint. |
| src/api/tools/commonTools.ts            | Changed log level for pre-generated file fetch result logging.      |
| src/api/test-setup.ts                   | Added test setup handler and dummy file uploads for E2E tests.      |
| playwright.config.ts                    | Configured Playwright for E2E testing and global setup file usage.    |
| .github/workflows/e2e-tests.yml         | Added GitHub workflow for running E2E tests on PR changes.           |
</details>


<details>
<summary>Files not reviewed (3)</summary>

* **package.json**: Language not supported
* **pnpm-lock.yaml**: Language not supported
* **wrangler.jsonc**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/api/tools/commonTools.ts:205**
* [nitpick] Using console.error here for a successful file fetch may be misleading; consider reverting to console.log or an info-level log to indicate a successful fetch rather than an error.
```
console.error(`Fetched pre-generated llms.txt for ${owner}/${repo}`);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T20:25:21Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces end-to-end testing infrastructure by adding new test files and updating configuration files to support E2E tests using Playwright and Vitest. Key changes include:
- Updates to Vitest config to exclude E2E tests from unit/integration runs.
- New global setup and E2E test files to drive tests against a deployed local worker.
- Additions to the main application code and test setup to support a dedicated test endpoint for R2 population.

### Reviewed Changes

Copilot reviewed 9 out of 12 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                                    |
| ----------------------------------- | -------------------------------------------------------------- |
| vitest.config.ts                    | Updated test exclude patterns to include tests/e2e/**/*        |
| tests/global-setup.ts               | Added global setup for preparing R2 state for tests              |
| tests/e2e/inspection.spec.ts        | New E2E test suite using Playwright to validate tool functionality|
| src/index.ts                        | Added route to handle R2 test setup in test environment          |
| src/api/tools/commonTools.ts         | Changed logging calls for retrieving pre-generated llms.txt      |
| src/api/test-setup.ts                | New test setup endpoint to upload dummy files to R2              |
| playwright.config.ts                | Added Playwright config to run E2E tests against the Inspector UI  |
| .github/workflows/e2e-tests.yml      | New CI workflow configuration for running E2E tests               |
</details>


<details>
<summary>Files not reviewed (3)</summary>

* **package.json**: Language not supported
* **pnpm-lock.yaml**: Language not supported
* **wrangler.jsonc**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**src/api/tools/commonTools.ts:205**
* The logging level was changed from console.log to console.error for a successfully fetched pre-generated file, which could be misleading. Consider reverting to console.log or using console.info if this is expected behavior.
```
console.error(`Fetched pre-generated llms.txt for ${owner}/${repo}`);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T20:48:50Z

Review comments:
diff_hunk: @@ -0,0 +1,85 @@
+export interface TestSetupBindings extends Env {
+  DOCS_BUCKET: R2Bucket; // Ensure this matches CloudflareEnvironment definition
+  ENVIRONMENT?: string; // Environment variable to check if we are in 'test' mode
+}
+
+const dummyFilesToUpload = [
+  {
+    bucketPath: "remix-run/react-router/llms.txt",
+    content:
+      "# React Router Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bicketPath: "answerdotai/fasthtml/llms.txt",
+    content:
+      "# FastHTML Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bicketPath: "mrdoob/three.js/llms.txt",
path: src/api/test-setup.ts
user: login: Copilot
body: The property name 'bicketPath' is misspelled; it should be 'bucketPath' to match the expected binding in the environment.
```suggestion
    bucketPath: "answerdotai/fasthtml/llms.txt",
    content:
      "# FastHTML Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
  },
  {
    bucketPath: "mrdoob/three.js/llms.txt",
```
created_at: 2025-04-26T20:25:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,85 @@
+export interface TestSetupBindings extends Env {
+  DOCS_BUCKET: R2Bucket; // Ensure this matches CloudflareEnvironment definition
+  ENVIRONMENT?: string; // Environment variable to check if we are in 'test' mode
+}
+
+const dummyFilesToUpload = [
+  {
+    bucketPath: "remix-run/react-router/llms.txt",
+    content:
+      "# React Router Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bicketPath: "answerdotai/fasthtml/llms.txt",
+    content:
+      "# FastHTML Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
+  },
+  {
+    bicketPath: "mrdoob/three.js/llms.txt",
path: src/api/test-setup.ts
user: login: Copilot
body: The property name 'bicketPath' is misspelled; please rename it to 'bucketPath' to ensure proper file uploads.
```suggestion
    bucketPath: "answerdotai/fasthtml/llms.txt",
    content:
      "# FastHTML Dummy Test File\n\nThis is hardcoded content for E2E testing R2 population.",
  },
  {
    bucketPath: "mrdoob/three.js/llms.txt",
```
created_at: 2025-04-26T20:25:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-25T00:42:08Z
body: 🚀 Cloudflare Preview deployed at: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T00:55:55Z
body: 🚀 Cloudflare Preview deployed at: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T00:59:51Z
body: 🚀 Cloudflare Preview deployed at: 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T01:02:41Z
body: 🚀 Cloudflare Preview deployed at: 
author_association: NONE