feat(FN-094): add comment line for deployment verification
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

- Added a comment line to main.ts for deployment verification purposes
This commit is contained in:
Fusion
2026-05-11 02:07:03 +00:00
parent c72f063a25
commit f4fea1e429
274 changed files with 20712 additions and 6305 deletions

View File

@@ -0,0 +1,79 @@
# Ralph VIN Test Loop
## Gorev
Dashboard'daki VIN cozme ozelliginin tum VIN'ler uzerinde uctan uca test edilmesi.
## Calisma Dongusu
### 1. Test Calistir
```bash
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --resume
```
### 2. Sonucu Degerlendir
**Basarili cikarsa** (exit code 0):
- Tum VIN'ler test edilmis demektir
- `ralpRapor.md` dosyasini kontrol et
- Islem tamamlandi
**Hata ile cikarsa** (exit code 1):
- `ralph-vin-test/.ralph/progress.json` dosyasindaki `errorVin` ve `errorMessage` alanlarini oku
- `ralpRapor.md` dosyasindaki "Hata Detaylari" bolumunu incele
- Hata turunu belirle:
- **Network/API hatasi (4xx/5xx):** API kaynak kodunu incele
- **UI render hatasi:** Web kaynak kodunu incele
- **Timeout:** Performans sorunu veya eksik veri
### 3. Fix Yap
- Hatanin kaynak kodunu bul (`apps/api/src/` veya `apps/web/src/`)
- Fix uygula
- Rebuild yap:
```bash
# API fix ise:
cd /home/s/ss && pnpm --filter api build
# Web fix ise:
cd /home/s/ss && pnpm --filter web build
```
### 4. Fix'i Dogrula
- Ayni VIN'i tekrar test et:
```bash
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --single <HATA_VIN>
```
- Basarili olursa devam, hata devam ederse tekrar fix
### 5. Commit & Push
```bash
cd /home/s/ss
git add -A
git commit -m "fix: <kisa aciklama>"
git push
```
### 6. Devam Et
```bash
cd /home/s/ss/ralph-vin-test && npx tsx run-all-vins.ts --resume
```
## Onemli Dosyalar
| Dosya | Rol |
|-------|-----|
| `apps/api/src/vehicles/vehicles.service.ts` | VIN decode zinciri |
| `apps/api/src/categories/categories.service.ts` | Kategori tree + parts fetch |
| `apps/api/src/integrations/pl24/pl24.service.ts` | PL24 API entegrasyonu |
| `apps/api/src/integrations/emex/emex.service.ts` | EMEX scraper entegrasyonu |
| `apps/web/src/routes/dashboard/search.tsx` | VIN arama sayfasi |
| `apps/web/src/routes/dashboard/vehicles_/$id/index.tsx` | Arac detay + kategori listesi |
| `apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx` | Kategori parts sayfasi |
| `apps/web/src/components/categories/category-grid.tsx` | Kategori grid bileseni |
| `apps/web/src/hooks/use-parts.ts` | Parts query hook |
## Kurallar
- Decode basarisiz veya kategori yok olan VIN'leri SKIP et (bunlar expected)
- Kategori hatalari (4xx/5xx, error banner, timeout) mutlaka fix'lenmeli
- Her fix sonrasi rebuild + verify + commit + push
- `ralpRapor.md` her iterasyonda guncel olmali
- `progress.json` her VIN sonrasi kaydedilmeli (resume icin)

File diff suppressed because it is too large Load Diff

617
ralph-vin-test/package-lock.json generated Normal file
View File

@@ -0,0 +1,617 @@
{
"name": "ralph-vin-test",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ralph-vin-test",
"dependencies": {
"playwright": "^1.50.0"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.6",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "ralph-vin-test",
"private": true,
"type": "module",
"scripts": {
"test": "npx tsx test-vin.ts",
"test:all": "npx tsx run-all-vins.ts"
},
"dependencies": {
"playwright": "^1.50.0"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,350 @@
/**
* run-all-vins.ts — Orchestrator that runs test-vin for each VIN in the list.
*
* Reads VINs from scripts/vin-test-list.md, tests each one, writes results
* to ralpRapor.md, and maintains progress.json for resume support.
*
* Usage: npx tsx run-all-vins.ts [--resume] [--single VIN] [--start N]
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { testVin, closeSession, type VinTestResult } from "./test-vin.js";
// ─── PATHS ──────────────────────────────────────────────────────────────────
const PROJECT_ROOT = path.resolve(import.meta.dirname, "..");
const VIN_LIST_PATH = path.join(PROJECT_ROOT, "scripts/vin-test-list.md");
const PROGRESS_PATH = path.join(import.meta.dirname, ".ralph/progress.json");
const REPORT_PATH = path.join(PROJECT_ROOT, "ralpRapor.md");
// ─── TYPES ──────────────────────────────────────────────────────────────────
interface Progress {
startedAt: string;
lastUpdated: string;
currentIndex: number;
totalVins: number;
results: Record<string, VinTestResult>;
status: "running" | "paused" | "completed" | "error";
errorVin?: string;
errorMessage?: string;
}
// ─── VIN LIST PARSER ────────────────────────────────────────────────────────
function parseVinList(): string[] {
const content = fs.readFileSync(VIN_LIST_PATH, "utf-8");
const lines = content.split("\n");
let inCodeBlock = false;
const vins: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "```") {
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock && /^[A-HJ-NPR-Z0-9]{17}$/i.test(trimmed)) {
vins.push(trimmed.toUpperCase());
}
}
// Also extract VINs from the table
for (const line of lines) {
const match = line.match(/\|\s*\w+\s*\|\s*([A-HJ-NPR-Z0-9]{17})\s*\|/i);
if (match) {
const vin = match[1].toUpperCase();
if (!vins.includes(vin)) vins.push(vin);
}
}
return [...new Set(vins)]; // Deduplicate
}
// ─── PROGRESS MANAGEMENT ────────────────────────────────────────────────────
function loadProgress(): Progress | null {
try {
const data = fs.readFileSync(PROGRESS_PATH, "utf-8");
return JSON.parse(data);
} catch {
return null;
}
}
function saveProgress(progress: Progress): void {
progress.lastUpdated = new Date().toISOString();
fs.writeFileSync(PROGRESS_PATH, JSON.stringify(progress, null, 2));
}
// ─── REPORT GENERATOR ───────────────────────────────────────────────────────
function generateReport(progress: Progress, vins: string[]): void {
const lines: string[] = [];
lines.push("# VIN Test Raporu (Ralph Loop)");
lines.push("");
lines.push(`**Baslangic:** ${progress.startedAt}`);
lines.push(`**Son Guncelleme:** ${progress.lastUpdated}`);
lines.push(`**Durum:** ${progress.status}`);
lines.push(`**Ilerleme:** ${Object.keys(progress.results).length} / ${vins.length}`);
lines.push("");
// Summary stats
const results = Object.values(progress.results);
const success = results.filter((r) => r.status === "success").length;
const partial = results.filter((r) => r.status === "partial").length;
const decodeFailed = results.filter((r) => r.status === "decode_failed").length;
const noCategories = results.filter((r) => r.status === "no_categories").length;
const errors = results.filter((r) => r.status === "error").length;
lines.push("## Ozet");
lines.push("");
lines.push(`| Metrik | Deger |`);
lines.push(`|--------|-------|`);
lines.push(`| Toplam Test | ${results.length} |`);
lines.push(`| Basarili | ${success} |`);
lines.push(`| Kismi Basarili | ${partial} |`);
lines.push(`| Decode Basarisiz | ${decodeFailed} |`);
lines.push(`| Kategori Yok | ${noCategories} |`);
lines.push(`| Hata | ${errors} |`);
lines.push("");
// Main results table
lines.push("## Detayli Sonuclar");
lines.push("");
lines.push("| # | VIN | Marka | Kategori Sayisi | Test Edilen | Basarili | Hatali | Durum | Sure |");
lines.push("|---|-----|-------|-----------------|-------------|----------|--------|-------|------|");
let idx = 1;
for (const vin of vins) {
const r = progress.results[vin];
if (!r) continue;
const statusEmoji =
r.status === "success" ? "OK" :
r.status === "partial" ? "KISMI" :
r.status === "decode_failed" ? "DECODE_FAIL" :
r.status === "no_categories" ? "KATEGORI_YOK" :
"HATA";
const duration = `${(r.duration / 1000).toFixed(1)}s`;
lines.push(
`| ${idx} | ${r.vin} | ${r.brandName || "-"} | ${r.totalCategories} | ${r.testedCategories} | ${r.successCategories} | ${r.errorCategories} | ${statusEmoji} | ${duration} |`,
);
idx++;
}
// Error details section
const allErrors = results.flatMap((r) =>
r.errors.map((e) => ({ vin: r.vin, ...e })),
);
if (allErrors.length > 0) {
lines.push("");
lines.push("## Hata Detaylari");
lines.push("");
lines.push("| VIN | Kategori | Yol | HTTP Kodu | Endpoint | Hata Detayi |");
lines.push("|-----|----------|-----|-----------|----------|-------------|");
for (const err of allErrors) {
lines.push(
`| ${err.vin} | ${err.categoryName} | ${err.path.join(" > ")} | ${err.httpCode || "-"} | ${err.endpoint || "-"} | ${err.errorDetail || "-"} |`,
);
}
}
// Stopped at error
if (progress.status === "error" && progress.errorVin) {
lines.push("");
lines.push("## Durdurulan Hata");
lines.push("");
lines.push(`**VIN:** ${progress.errorVin}`);
lines.push(`**Mesaj:** ${progress.errorMessage || "Unknown"}`);
lines.push("");
lines.push("Bu VIN icin fix yapilmasi gerekiyor. Fix yapildiktan sonra `--resume` ile devam edilebilir.");
}
lines.push("");
fs.writeFileSync(REPORT_PATH, lines.join("\n"));
}
// ─── MAIN ───────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const args = process.argv.slice(2);
const resumeMode = args.includes("--resume");
const singleVinIdx = args.indexOf("--single");
const singleVin = singleVinIdx !== -1 ? args[singleVinIdx + 1] : null;
const startIdx = args.indexOf("--start");
const startFrom = startIdx !== -1 ? parseInt(args[startIdx + 1], 10) : 0;
// Parse VIN list
const allVins = parseVinList();
console.log(`Parsed ${allVins.length} VINs from test list`);
if (singleVin) {
console.log(`\nTesting single VIN: ${singleVin}`);
const result = await testVin(singleVin);
console.log(JSON.stringify(result, null, 2));
await closeSession();
return;
}
// Load or create progress
let progress: Progress;
if (resumeMode) {
const existing = loadProgress();
if (existing) {
progress = existing;
progress.status = "running";
console.log(`Resuming from VIN index ${progress.currentIndex}`);
} else {
console.log("No existing progress found, starting fresh");
progress = {
startedAt: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
currentIndex: startFrom,
totalVins: allVins.length,
results: {},
status: "running",
};
}
} else {
progress = {
startedAt: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
currentIndex: startFrom,
totalVins: allVins.length,
results: {},
status: "running",
};
}
// Run tests
for (let i = progress.currentIndex; i < allVins.length; i++) {
const vin = allVins[i];
progress.currentIndex = i;
// Skip already tested
if (progress.results[vin]) {
console.log(`[${i + 1}/${allVins.length}] Skipping ${vin} (already tested)`);
continue;
}
console.log(`\n${"=".repeat(70)}`);
console.log(`[${i + 1}/${allVins.length}] Testing VIN: ${vin}`);
console.log(`${"=".repeat(70)}`);
const result = await testVin(vin);
progress.results[vin] = result;
// Log result summary
const statusIcon =
result.status === "success" ? "[OK]" :
result.status === "partial" ? "[PARTIAL]" :
result.status === "decode_failed" ? "[DECODE_FAIL]" :
result.status === "no_categories" ? "[NO_CAT]" :
"[ERROR]";
console.log(
`${statusIcon} ${vin} | Categories: ${result.testedCategories}/${result.totalCategories} | Errors: ${result.errorCategories} | ${(result.duration / 1000).toFixed(1)}s`,
);
if (result.message) {
console.log(` Message: ${result.message}`);
}
if (result.errors.length > 0) {
for (const err of result.errors) {
console.log(` ERROR: ${err.path.join(" > ")} | ${err.errorDetail}`);
}
}
// Save progress after each VIN
saveProgress(progress);
generateReport(progress, allVins);
// Stop on critical errors (not decode_failed or no_categories, which are expected)
if (result.status === "error") {
const msg = (result.message || "").toLowerCase();
const isTransient = msg.includes("econnrefused") ||
msg.includes("context was destroyed") ||
msg.includes("browser has been closed") ||
msg.includes("target page") ||
msg.includes("execution context") ||
msg.includes("socket hang up") ||
msg.includes("navigation");
if (isTransient) {
console.log(`\n [TRANSIENT] ${result.message} — closing session, will retry next VIN`);
// Remove from results so it gets retested on resume
delete progress.results[vin];
saveProgress(progress);
await closeSession();
// Wait 10s for server to recover
await new Promise((r) => setTimeout(r, 10_000));
continue;
}
console.log(`\n*** CRITICAL ERROR on VIN ${vin} ***`);
console.log(`Message: ${result.message}`);
progress.status = "error";
progress.errorVin = vin;
progress.errorMessage = result.message || "Unknown error";
saveProgress(progress);
generateReport(progress, allVins);
console.log("\nStopping. Fix the issue and run with --resume to continue.");
await closeSession();
process.exit(1);
}
// Stop on partial only if there are real API errors (4xx/5xx), not timeouts or connectivity
if (result.status === "partial" && result.errorCategories > 0) {
// Filter out transient errors: 408 (PL24 timeout), 429 (rate limit), and connectivity errors (no httpCode)
const httpErrors = result.errors.filter((e) => {
if (!e.httpCode) return false; // no httpCode = connectivity issue (ECONNREFUSED, etc.)
if (e.httpCode === 408 || e.httpCode === 429) return false; // transient
return e.httpCode >= 400;
});
if (httpErrors.length > 0) {
console.log(`\n*** CATEGORY ERRORS found on VIN ${vin} ***`);
console.log(`${httpErrors.length} categories had HTTP errors.`);
progress.status = "error";
progress.errorVin = vin;
progress.errorMessage = `${httpErrors.length} HTTP errors: ${httpErrors.map((e) => e.errorDetail).join("; ")}`;
saveProgress(progress);
generateReport(progress, allVins);
console.log("\nStopping. Fix the issue and run with --resume to continue.");
await closeSession();
process.exit(1);
}
// Non-HTTP errors (timeouts, connectivity, etc.) — log but continue
console.log(` (${result.errorCategories} non-critical errors, continuing)`);
}
}
// All done
progress.status = "completed";
progress.currentIndex = allVins.length;
saveProgress(progress);
generateReport(progress, allVins);
console.log(`\n${"=".repeat(70)}`);
console.log("ALL VINs TESTED SUCCESSFULLY");
console.log(`${"=".repeat(70)}`);
console.log(`Report saved to: ${REPORT_PATH}`);
await closeSession();
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

549
ralph-vin-test/test-vin.ts Normal file
View File

@@ -0,0 +1,549 @@
/**
* test-vin.ts — Playwright VIN tester using grid UI navigation.
*
* Strategy:
* 1. Login → decode VIN → vehicle page
* 2. Grid modunda kategorileri göster
* 3. Her kart:
* - Branch (button) → tıkla, drill-down, recursive test, breadcrumb back
* - Leaf (a link) → API ile parts endpoint'ini test et
* 4. Index-based kart seçimi (multiline isim sorunu yok)
*
* Rate limiting: sase.tr 100 req/60s. Grid UI kendi fetch'lerini yapar,
* biz sadece leaf test API call'ları yapıyoruz → çok daha az istek.
*
* Usage: npx tsx test-vin.ts <VIN>
*/
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
// ─── TYPES ──────────────────────────────────────────────────────────────────
export interface CategoryTestResult {
categoryId: string;
categoryName: string;
path: string[];
status: "ok" | "error" | "empty";
errorDetail?: string;
httpCode?: number;
endpoint?: string;
}
export interface VinTestResult {
vin: string;
status: "success" | "decode_failed" | "no_categories" | "error" | "partial";
vehicleId?: string;
brandName?: string;
totalCategories: number;
testedCategories: number;
successCategories: number;
errorCategories: number;
errors: CategoryTestResult[];
duration: number;
message?: string;
}
// ─── CONFIG ─────────────────────────────────────────────────────────────────
const BASE_URL = "http://localhost:3000";
const LOGIN_EMAIL = "admin@sase.tr";
const LOGIN_PASSWORD = "Sase2026";
const VIN_DECODE_TIMEOUT = 120_000;
const PAGE_LOAD_TIMEOUT = 30_000;
// ─── RATE LIMITER ───────────────────────────────────────────────────────────
const RATE_WINDOW = 60_000;
const RATE_MAX = 85;
const timestamps: number[] = [];
async function rateWait(): Promise<void> {
const now = Date.now();
while (timestamps.length > 0 && timestamps[0] < now - RATE_WINDOW) timestamps.shift();
if (timestamps.length >= RATE_MAX) {
const wait = timestamps[0] + RATE_WINDOW - Date.now() + 200;
if (wait > 0) await sleep(wait);
while (timestamps.length > 0 && timestamps[0] < Date.now() - RATE_WINDOW) timestamps.shift();
}
timestamps.push(Date.now());
}
// ─── SESSION ────────────────────────────────────────────────────────────────
let sharedCtx: BrowserContext | null = null;
let sharedBrowser: Browser | null = null;
export async function getOrCreateSession(): Promise<{ browser: Browser; context: BrowserContext }> {
if (sharedBrowser && sharedCtx) {
try {
const p = await sharedCtx.newPage();
await p.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: 10_000 });
await p.waitForTimeout(2000);
const ok = p.url().includes("/dashboard/search");
await p.close();
if (ok) return { browser: sharedBrowser, context: sharedCtx };
} catch {}
}
if (sharedBrowser) try { await sharedBrowser.close(); } catch {}
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: BASE_URL, viewport: { width: 1280, height: 900 } });
const page = await context.newPage();
await loginViaUI(page);
await page.close();
sharedBrowser = browser;
sharedCtx = context;
return { browser, context };
}
async function loginViaUI(page: Page): Promise<void> {
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15_000 });
await page.waitForTimeout(1000);
await page.fill('#email', LOGIN_EMAIL);
await page.fill('#password', LOGIN_PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL("**/dashboard/**", { timeout: 15_000 });
console.log(" Login OK");
}
export async function closeSession(): Promise<void> {
if (sharedBrowser) try { await sharedBrowser.close(); } catch {}
sharedBrowser = null;
sharedCtx = null;
}
function sleep(ms: number): Promise<void> { return new Promise((r) => setTimeout(r, ms)); }
// ─── GRID HELPERS ───────────────────────────────────────────────────────────
/** Wait for grid to be loaded (no spinner, cards visible) */
async function waitForGrid(page: Page, timeout = PAGE_LOAD_TIMEOUT): Promise<void> {
try {
await page.waitForFunction(
() => !document.querySelector('.animate-spin'),
{ timeout },
);
} catch {}
await page.waitForTimeout(600);
}
/** Wait for drill-down: the grid content should change after clicking a branch */
async function waitForDrillDown(page: Page, prevCardNames: string[], timeout = 15_000): Promise<boolean> {
try {
await page.waitForFunction(
(prevNames) => {
const noSpinner = !document.querySelector('.animate-spin');
if (!noSpinner) return false;
// Check if breadcrumb appeared (means we drilled down)
const breadcrumb = document.querySelector('.flex.items-center.gap-1\\.5.text-sm');
if (breadcrumb) {
// Grid cards changed or breadcrumb appeared
const grid = document.querySelector('.grid.gap-3.lg\\:grid-cols-3');
if (!grid) return false;
const names = Array.from(grid.children).map(el => {
const n = el.querySelector('.font-semibold, .font-medium');
return n?.textContent?.trim() || '';
});
// Cards should be different from previous level
return JSON.stringify(names) !== JSON.stringify(prevNames);
}
return false;
},
prevCardNames,
{ timeout },
);
await page.waitForTimeout(300);
return true;
} catch {
return false;
}
}
/** Check if breadcrumb is visible (we're in a drilled-down state) */
async function hasBreadcrumb(page: Page): Promise<boolean> {
return page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
}
interface CardInfo {
index: number;
name: string;
isLeaf: boolean; // <a> = leaf, <button> = branch
categoryId: string; // extracted from href for leaves
}
/** Get all category cards currently visible in the grid */
async function getGridCards(page: Page): Promise<CardInfo[]> {
return page.evaluate(() => {
const grid = document.querySelector('.grid.gap-3.lg\\:grid-cols-3');
if (!grid) return [];
const items = Array.from(grid.children) as HTMLElement[];
return items.map((el, i) => {
const isLeaf = el.tagName.toLowerCase() === 'a';
const nameEl = el.querySelector('.font-semibold, .font-medium');
const name = nameEl?.textContent?.trim() || `Card ${i}`;
let categoryId = '';
if (isLeaf) {
const href = el.getAttribute('href') || '';
const match = href.match(/categories\/([^/]+)/);
if (match) categoryId = match[1];
}
return { index: i, name, isLeaf, categoryId };
});
});
}
/** Click a card by index in the grid */
async function clickCard(page: Page, index: number): Promise<boolean> {
const card = page.locator('.grid.gap-3.lg\\:grid-cols-3 > *').nth(index);
try {
await card.click({ timeout: 5000 });
return true;
} catch {
return false;
}
}
/** Go back one level via the breadcrumb back arrow */
async function gridGoBack(page: Page): Promise<void> {
// ArrowLeft button in breadcrumb area: first button inside .flex.items-center.gap-1\.5
const backBtn = page.locator('.flex.items-center.gap-1\\.5.text-sm button').first();
if (await backBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await backBtn.click();
await page.waitForTimeout(400);
return;
}
// Fallback: "Kategoriler" text button (goes to root)
const rootBtn = page.locator('button:has-text("Kategoriler")');
if (await rootBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await rootBtn.click();
await page.waitForTimeout(400);
}
}
// ─── RECURSIVE GRID TESTER ─────────────────────────────────────────────────
let leafCounter = 0;
async function testGridLevel(
page: Page,
vehicleId: string,
pathSoFar: string[],
results: CategoryTestResult[],
depth: number = 0,
): Promise<void> {
if (depth > 10) return;
await waitForGrid(page);
const initialCards = await getGridCards(page);
if (initialCards.length === 0) return;
// Track processed cards by name to survive grid re-reads
const processed = new Set<string>();
// First pass: test all leaves (no navigation needed, just API calls)
for (const card of initialCards) {
if (card.isLeaf) {
processed.add(card.name);
leafCounter++;
const currentPath = [...pathSoFar, card.name];
const result = await testLeafApi(page, vehicleId, card.categoryId, card.name, currentPath);
if (result.httpCode === 429) {
console.log(` [${leafCounter}] Rate limited, waiting 60s...`);
await sleep(60_000);
const retry = await testLeafApi(page, vehicleId, card.categoryId, card.name, currentPath);
results.push(retry);
if (retry.status === "error") console.log(` [${leafCounter}] ERROR: ${card.name} | ${retry.errorDetail}`);
} else {
results.push(result);
if (result.status === "error") {
console.log(` [${leafCounter}] ERROR: ${card.name} | ${result.errorDetail}`);
} else if (leafCounter % 50 === 0) {
console.log(` [${leafCounter}] OK...`);
}
}
}
}
// Second pass: drill into each branch
// Re-read cards fresh before each branch click (grid state may have changed)
const branchNames = initialCards.filter(c => !c.isLeaf).map(c => c.name);
for (const branchName of branchNames) {
if (processed.has(branchName)) continue;
processed.add(branchName);
// Re-read grid to find the branch by name (index may have shifted)
const freshCards = await getGridCards(page);
const branchIdx = freshCards.findIndex(c => c.name === branchName && !c.isLeaf);
if (branchIdx === -1) {
console.log(` ${" ".repeat(depth)}> ${branchName} (not found in grid, skipping)`);
continue;
}
const currentPath = [...pathSoFar, branchName];
const prevNames = freshCards.map(c => c.name);
console.log(` ${" ".repeat(depth)}> ${branchName}`);
const clicked = await clickCard(page, branchIdx);
if (!clicked) {
// Retry: wait a bit and try again
await page.waitForTimeout(1000);
const retryCards = await getGridCards(page);
const retryIdx = retryCards.findIndex(c => c.name === branchName && !c.isLeaf);
if (retryIdx === -1 || !(await clickCard(page, retryIdx))) {
console.log(` ${" ".repeat(depth)} (click failed, skipping)`);
continue;
}
}
// Wait for drill-down
const drilled = await waitForDrillDown(page, prevNames);
if (!drilled) {
await waitForGrid(page);
const afterCards = await getGridCards(page);
const afterNames = afterCards.map(c => c.name);
if (JSON.stringify(afterNames) === JSON.stringify(prevNames)) {
console.log(` ${" ".repeat(depth)} (drill-down failed, skipping)`);
continue;
}
}
await waitForGrid(page);
const subCards = await getGridCards(page);
console.log(` ${" ".repeat(depth)} -> ${subCards.length} children`);
if (subCards.length > 0) {
await testGridLevel(page, vehicleId, currentPath, results, depth + 1);
}
// Go back to current level
const restored = await navigateBackToLevel(page, prevNames, depth);
if (!restored) {
console.log(` ${" ".repeat(depth)} (nav lost, resetting...)`);
// Hard reset: reload page and re-navigate to current path
await resetToRoot(page, vehicleId);
if (pathSoFar.length > 0) {
const renavOk = await navigateToPath(page, pathSoFar);
if (!renavOk) {
console.log(` ${" ".repeat(depth)} (re-nav failed, stopping this branch)`);
return;
}
}
// Now we should be at the correct level, continue to next branch
}
}
}
/** Navigate back and verify we're at the correct level */
async function navigateBackToLevel(page: Page, expectedNames: string[], depth: number): Promise<boolean> {
await gridGoBack(page);
await page.waitForTimeout(400);
await waitForGrid(page);
let currentCards = await getGridCards(page);
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
// Try a few more back clicks
for (let i = 0; i < 3; i++) {
const hasBc = await page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
if (!hasBc && depth === 0) {
// At root, check if cards match
currentCards = await getGridCards(page);
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
break;
}
await gridGoBack(page);
await page.waitForTimeout(400);
await waitForGrid(page);
currentCards = await getGridCards(page);
if (currentCards.map(c => c.name).join('|') === expectedNames.join('|')) return true;
}
return false;
}
/** Reset grid to root level — hard reload as fallback */
async function resetToRoot(page: Page, vehicleId?: string): Promise<void> {
// Try breadcrumb "Kategoriler" button first
const rootBtn = page.locator('button:has-text("Kategoriler")');
if (await rootBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await rootBtn.click();
await page.waitForTimeout(500);
await waitForGrid(page);
// Verify we're at root (no breadcrumb visible)
const hasBc = await page.locator('.flex.items-center.gap-1\\.5.text-sm').isVisible({ timeout: 500 }).catch(() => false);
if (!hasBc) return;
}
// Hard fallback: reload the vehicle page
if (vehicleId) {
await page.goto(`${BASE_URL}/dashboard/vehicles/${vehicleId}`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
await page.waitForSelector('.grid.gap-3.lg\\:grid-cols-3', { timeout: PAGE_LOAD_TIMEOUT }).catch(() => {});
await waitForGrid(page);
}
}
/** Navigate from root to a specific path by clicking branches one by one */
async function navigateToPath(page: Page, pathSoFar: string[]): Promise<boolean> {
for (const name of pathSoFar) {
await waitForGrid(page);
const cards = await getGridCards(page);
const idx = cards.findIndex(c => c.name === name && !c.isLeaf);
if (idx === -1) return false;
const prevNames = cards.map(c => c.name);
const clicked = await clickCard(page, idx);
if (!clicked) return false;
const drilled = await waitForDrillDown(page, prevNames);
if (!drilled) {
// Check if cards actually changed
const afterCards = await getGridCards(page);
if (JSON.stringify(afterCards.map(c => c.name)) === JSON.stringify(prevNames)) return false;
}
}
return true;
}
/** Test a leaf category's parts endpoint via API */
async function testLeafApi(
page: Page,
vehicleId: string,
categoryId: string,
name: string,
path: string[],
): Promise<CategoryTestResult> {
const endpoint = `/api/vehicles/${vehicleId}/categories/${categoryId}`;
try {
await rateWait();
const resp = await page.request.get(`${BASE_URL}${endpoint}`, { timeout: 120_000 });
const status = resp.status();
if (status >= 400) {
return { categoryId, categoryName: name, path, status: "error", errorDetail: `HTTP ${status} ${resp.statusText()}`, httpCode: status, endpoint };
}
let data: any;
try {
data = await resp.json();
if (data.data) data = data.data;
} catch {
return { categoryId, categoryName: name, path, status: "error", errorDetail: "JSON parse failed", httpCode: status, endpoint };
}
return { categoryId, categoryName: name, path, status: "ok" };
} catch (err: any) {
return { categoryId, categoryName: name, path, status: "error", errorDetail: `Request failed: ${err.message}`, endpoint };
}
}
// ─── MAIN ───────────────────────────────────────────────────────────────────
export async function testVin(vin: string): Promise<VinTestResult> {
const startTime = Date.now();
const results: CategoryTestResult[] = [];
leafCounter = 0;
let page: Page | null = null;
try {
const { context } = await getOrCreateSession();
page = await context.newPage();
// ── Step 1: Search page ─────────────────────────────────────────────
await page.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
if (!page.url().includes("/dashboard/search")) {
await loginViaUI(page);
await page.goto(`${BASE_URL}/dashboard/search`, { waitUntil: "domcontentloaded", timeout: PAGE_LOAD_TIMEOUT });
}
// ── Step 2: Enter VIN ───────────────────────────────────────────────
const vinInput = page.locator('input[placeholder*="17 karakter"]');
await vinInput.waitFor({ state: "visible", timeout: 10_000 });
await vinInput.fill(vin);
await page.waitForTimeout(500);
await page.locator('button[type="submit"]:has-text("Çöz")').click();
// ── Step 3: Wait for vehicle page ───────────────────────────────────
try {
await page.waitForURL("**/dashboard/vehicles/**", { timeout: VIN_DECODE_TIMEOUT });
} catch {
const errEl = page.locator(".text-destructive");
if (await errEl.isVisible({ timeout: 2000 }).catch(() => false)) {
const msg = await errEl.first().textContent().catch(() => "Unknown");
return mkResult(vin, "decode_failed", startTime, { message: `Decode failed: ${msg}` });
}
return mkResult(vin, "decode_failed", startTime, { message: "Decode timeout" });
}
const vehicleId = page.url().match(/\/vehicles\/([^/]+)/)?.[1] || "";
const brandName = (await page.locator("h2").first().textContent().catch(() => ""))?.split(" ")[0] || "";
// ── Step 4: Wait for categories grid ────────────────────────────────
// Wait for skeleton to disappear and grid to appear
try {
await page.waitForSelector('.grid.gap-3.lg\\:grid-cols-3', { timeout: PAGE_LOAD_TIMEOUT });
} catch {
// Check "Kategori bulunamadi"
const nocat = page.locator('text="Kategori bulunamadi."');
if (await nocat.isVisible({ timeout: 2000 }).catch(() => false)) {
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "Kategori bulunamadi" });
}
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "Grid did not load" });
}
await waitForGrid(page);
const rootCards = await getGridCards(page);
console.log(` Vehicle: ${brandName} | ${rootCards.length} root categories`);
if (rootCards.length === 0) {
return mkResult(vin, "no_categories", startTime, { vehicleId, brandName, message: "No root categories" });
}
// ── Step 5: Recursively test all categories via grid ────────────────
await testGridLevel(page, vehicleId, [], results, 0);
console.log(` Done: ${leafCounter} leaves tested`);
const errs = results.filter((r) => r.status === "error");
return {
vin,
status: errs.length > 0 ? "partial" : "success",
vehicleId,
brandName,
totalCategories: leafCounter,
testedCategories: results.length,
successCategories: results.filter((r) => r.status === "ok").length,
errorCategories: errs.length,
errors: errs,
duration: Date.now() - startTime,
};
} catch (err: any) {
return {
vin,
status: "error",
totalCategories: 0,
testedCategories: results.length,
successCategories: results.filter((r) => r.status === "ok").length,
errorCategories: results.filter((r) => r.status === "error").length,
errors: results.filter((r) => r.status === "error"),
duration: Date.now() - startTime,
message: `Unexpected: ${err.message}`,
};
} finally {
if (page) try { await page.close(); } catch {}
}
}
function mkResult(vin: string, status: VinTestResult["status"], start: number, o: Partial<VinTestResult> = {}): VinTestResult {
return { vin, status, totalCategories: 0, testedCategories: 0, successCategories: 0, errorCategories: 0, errors: [], duration: Date.now() - start, ...o };
}
// ─── CLI ─────────────────────────────────────────────────────────────────────
if (process.argv[1]?.endsWith("test-vin.ts")) {
const vin = process.argv[2];
if (!vin) { console.error("Usage: npx tsx test-vin.ts <VIN>"); process.exit(1); }
console.log(`Testing VIN: ${vin}`);
testVin(vin)
.then((r) => { console.log(JSON.stringify(r, null, 2)); return closeSession(); })
.then(() => process.exit(0))
.catch((e) => { console.error("Fatal:", e); process.exit(1); });
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["*.ts"]
}