# 03 — QA: dal doğrulaması, kod denetimi ve lokal duman testi Dal `gece/eval-jev` · incelenen değişiklik `6564ec8~1..b935158` (danışman sohbeti AI SDK'ya taşındı, 11 araç, Jev çıktı denetimi) · 5 Ekim 2026 gecesi · QA mühendisi ## Özet (5 satır) 1. **Dal derleniyor ve ana akış çalışıyor.** typegen + tsc + lint + build hatasız; gerçek modelle 7 soru soruldu: araç çağrısı akışta görünüyor, kredi doğru düşüyor, cevap kaydı ekrandakiyle aynı, Jev satırı her cevapta logda. 2. **En önemli hata — profil kaydı kayboluyor:** danışman iki tercihi aynı anda kaydedince biri siliniyor; ekranda "Kaydettim: Ankara, 300 km" yazıyor, veritabanında yalnız `maxKm` var (H1). Ayrı bir soruda da hiç kaydetmeden "Kaydettim" dedi (H4). 3. **Kredi tarafında iki delik:** sağlayıcı takılırsa 120 sn sonra yarım cevap "tamam" sayılıyor, kredi iade edilmiyor (H2); "evet. " yazan kullanıcı 0 kredisiyle tam cevap alıyor (H3). 4. **Çalışan korumalar:** model hatası / kopma / boş cevapta kredi iadesi, araç döngüsü sınırı (4 adım), bozuk araç argümanı, SQL enjeksiyonu, yetkisiz profil alanı, girişsiz istek (401) — hepsi çalıştırılarak doğrulandı. 5. **Test edilemeyen:** tarayıcıdaki `useChat` davranışı (kural gereği tarayıcı açılmadı); istemci bulguları "koddan çıkarım" diye işaretli. ## 1. Dal doğrulaması | Adım | Sonuç | |---|---| | `pnpm exec next typegen` | geçti | | `pnpm exec tsc --noEmit` | geçti, 0 hata | | `pnpm lint` (eslint + tasarım denetimi) | 0 hata. 1 eslint uyarısı: `src/lib/ai/cagri.ts:1` kullanılmayan `OpenAI` importu (bu değişiklikten kalma). Tasarım denetimi 12 uyarı, hepsi eski dosyalarda (`secimlerim-paneli.tsx`, `meraklisina-demo.tsx`) | | `pnpm build` (tek build) | geçti, 1935 statik sayfa. Uyarılar: (a) iki `pnpm-lock.yaml` yüzünden kök dizin tahmini — worktree'ye özgü; (b) `Cal Sans` için yedek font ölçüsü bulunamadı | | `pnpm start` | çalışıyor ama `"next start" does not work with "output: standalone"` uyarısı veriyor (üretim `node server.js` kullanıyor) | ## 2. Hata listesi Önem: kritik yok. Aksi yazılmadıkça hepsi **çalıştırılarak doğrulandı**. ### Yüksek **H1 — Aynı adımda iki `profil_guncelle` çağrısı birbirini eziyor** - Adım: "Ben Ankara'da yaşıyorum ve evden en fazla 300 km uzakta okumak istiyorum." → danışman onay sordu → "evet". - Beklenen: profilde `evIl: ANKARA` ve `maxKm: 300`. - Gerçekleşen: model iki aracı aynı adımda çağırdı; veritabanında yalnız `{"maxKm":300}`. Ekrandaki cevap: "Kaydettim: Yaşadığın il **Ankara**, evden en fazla **300 km**". `evIl` olmadan km süzgeci hiç uygulanmıyor (`araclar.ts:387`), yani kayıt fiilen etkisiz. - Kanıt: araç çıktıları `{"profil":["Yaşadığı il: Ankara"]}` ve `{"profil":["Evden en fazla 300 km"]}` — her biri yalnız kendi alanını görüyor; `GET /api/soru/profil` → `{"profil":{"maxKm":300}}`. - Yer: `src/lib/danisman-profil-db.ts:29-48` (oku → değiştir → yaz, işlem/kilit yok); çağıran `src/lib/ai/araclar.ts:799`. **H2 — Sağlayıcı takılınca (120 sn zaman aşımı) yarım cevap "tamamlandı" sayılıyor, kredi iade edilmiyor** - Adım: sahte sağlayıcı birkaç kelime yollayıp sustu. - Beklenen: hata + kredi iadesi (kopma ve 500'de olduğu gibi). - Gerçekleşen: 120,4 sn sonra akışta `abort` parçası, ardından `finish`; "Bu cevap takılacak ve " tam cevap gibi kaydedildi, kredi 6 → 5 kaldı, iade satırı yok, Jev de bu yarım metni denetledi. - Yer: `src/lib/ai/sohbet-akisi.ts:71-85` — döngü yalnız `error` parçasını hata sayıyor, `abort` parçasını saymıyor; süre `:56`. Metin hiç gelmeden takılırsa "Boş cevap" yoluna düşüp iade olur (koddan çıkarım). **H3 — "Evet" bedelsiz yolu kredisiz soru sormaya izin veriyor** - Adım: kredi 0; son danışman mesajı "…profiline kaydedeyim mi?" → "evet. Ege hukuk taban sırası kaç?" (33 karakter). - Beklenen: 402 "Kredin bitti" ya da yalnız onayın işlenmesi. - Gerçekleşen: `X-Kredi-Dusuldu: 0`, `program_ara` çağrıldı, 450 karakterlik tam cevap geldi; kredi 0 → 0. - Yer: `src/app/api/soru/route.ts:49-54` (yalnız "evet…" ile başlamasına ve 40 karaktere bakıyor), `:252-257` (bedelsiz yolda bakiye kontrolü yok). İstemci 0 kredide göndermeyi engelliyor ama uç doğrudan çağrılabilir. `/api/soru`'da hız sınırı da yok (koddan çıkarım: route'ta sınırlayıcı yok), dolayısıyla bu yol sınırsız tekrarlanabilir. **H4 — Danışman kaydetmeden "Kaydettim" diyor** (bir kez gözlendi) - Adım: "Vakıf üniversitesine yıllık en fazla 400 bin TL ödeyebilirim." → onay sorusu → "evet. Ege hukuk taban sırası kaç?" - Beklenen: `profil_guncelle(ucretUst)` çağrısı, sonra "Kaydettim". - Gerçekleşen: cevap "Kaydettim: Vakıfta yıllık ücret üst sınırı **400.000 TL**" ile başlıyor; akışta yalnız `program_ara` var, profilde `ucretUst` yok. - Yer: kayıt tamamen modelin sözüne bırakılmış (`route.ts:100-109` prompt kuralı); sunucu "Kaydettim" ile gerçek araç çağrısını karşılaştırmıyor. Eval setine girmeli. ### Orta **M1 — İstemci koparsa sunucu üretmeye devam ediyor; istemci yanlış bilgi veriyor** - Doğrulanan: istek 1,2 sn'de kesildi; sunucu cevabı sonuna kadar üretti (1.927 karakter), kaydetti, kredi düşük kaldı. İptal sinyali modele iletilmiyor (`sohbet-akisi.ts:56` yalnız zaman aşımı sinyali). - Koddan çıkarım: istemci kopunca hemen `GET /api/soru` yapıyor (`sohbet-client.tsx:322-341`); cevap o an henüz kaydedilmediği için "Kredin harcandıysa otomatik iade edildi" diyor ve soruyu kutuya geri koyuyor. İade yok; kullanıcı yeniden gönderirse aynı soruya iki kredi öder. **M2 — 100 mesajı aşan geçmişte en yeni mesajlar görünmüyor** - Adım: test kullanıcısında 121 mesaj. `GET /api/soru` 100 mesaj döndü, sonuncusu "qa dolgu 69"; veritabanındaki son mesaj "qa dolgu 90". `/listem` sunucu çıktısında da aynı. - Yer: `src/app/api/soru/route.ts:176-177`, `src/features/rapor/rapor-queries.ts:53-54` (eskiden yeniye sıralayıp ilk 100'ü alıyor). Bu değişiklikten eski, ama yeni istemcinin kopma sonrası toparlama mantığı (`sohbet-client.tsx:330`, "son mesaj danışmanın mı") buna dayanıyor. Paketli kullanıcı 60+30 krediyle bu sınıra varır. **M3 — Başarısız sorular geçmişte kalıyor, modele bozuk sıra gidiyor** - Hata/iade sonrası kullanıcı mesajı silinmiyor. Sahte sağlayıcı günlüğünde modele giden roller: `s-u-a-u-a-u-a-u-a-u-u-u-a-u` (art arda üç kullanıcı mesajı) ve `s-a-u-a…` (12'lik pencere danışman mesajıyla başlıyor). - Yer: `route.ts:230-235` ve `:359-365` (pencere sayıyla kesiliyor), `src/lib/credits.ts:114` (mesaj kredi düşümüyle yazılıyor, iade edilince kalıyor). **M4 — `sayiParse` "300 km"yi 300.000 okuyor** - `profil_guncelle {alan:"maxKm", deger:"300 km"}` → "Değer kabul edilmedi". "km"nin "k"si "bin" eki sayılıyor. - Yer: `src/lib/ai/araclar.ts:771`. **M5 — Araç argümanları şemaya göre doğrulanmıyor** - `jsonSchema(parameters)` doğrulayıcısız (`araclar.ts:861`; `inputSchema.validate` tanımsız). Sonuçlar: - yanlış tip (`arama: 12345`, `il: 35`) → `TypeError` → modele "Araç çalışmadı; veriye ulaşılamadı" (yanıltıcı: veri var, argüman bozuk); - geçersiz `dilim` → süzgeç etiketi `"dilim = undefined"`, sessizce Güvenli dilimi uygulanıyor (`:436-442`); - `ucret_ust: -5` kabul ediliyor ("yıllık ücret ≤ -5 TL"), `0` sessizce yok sayılıyor (`:418-423`); - `tur: "__proto__"` / `"constructor"` fırlatıyor (`:395`, `in` denetimi). - Hepsi yakalanıyor, cevap düşmüyor; ama model yanlış süzgeçle doğru görünen sonuç alabiliyor. ### Düşük - **L1 — Türkçe karaktersiz yazım tutarsız.** `program_ara il:"IZMIR"` → "İl tanınmadı"; `arama:"isletme"` → 0 sonuç; `il_ozeti "ISTANBUL"` çalışıyor ama `il_ozeti "Afyon"` bulunamıyor (`program_ara` aynı girdiyi buluyor). `program_ara universite:"ODTÜ"` → 0 (kısaltma yalnız `universite_profili`'nde). Yer: `araclar.ts:322-338, 380, 648`. - **L2 — Kısa/boş girdide yanlış kayıt dönüyor.** `bolum_ozeti "a"` ve `"---"` → Abaza Dili ve Edebiyatı; `bolum_istihdam ["a"]` → Acil Yardım; `universite_profili "a"` → Abdullah Gül. Yer: `araclar.ts:598-601, 623-626, 735-738` (boş slug her şeyle "başlıyor"). - **L3 — Kısaltma tablosunda bozuk satır:** `dtu: "deu"` hiçbir üniversiteyle eşleşmiyor (`araclar.ts:732`). - **L4 — Uzun girdi araç sonucuna aynen yansıyor** (20.000 karakter girdi → 20 KB sonuç). Yer: `araclar.ts:370, 628, 792`. - **L5 — LIKE jokerleri kaçışsız:** `arama:"%"` tüm programları eşliyor. Zararsız. SQL enjeksiyonu denendi (`' OR 1=1 --`, sıralama/dilim alanları) — geçmiyor, tüm değerler bağlı parametre. - **L6 — Hata logu tam hata nesnesini basıyor** (`sohbet-akisi.ts:107`), üstteki yorumun tersine. Mesaj içerikleri `[Object]` olarak kapalı; adres, model adı, cevap başlıkları görünüyor. - **L7 — Kayıtlı cevap baştaki boş satırlarla yazılıyor** (araç öncesi boş metin parçası; `sohbet-akisi.ts:74-78`). Ekranda fark yok. - **L8 — Jev eşiği dar:** anlamsız nötr cümle 0,42 aldı (eşik 0,50; raporlanan temiz üst sınır 0,37). Gerçek 7 cevabın 2'sine not eklendi (0,57 ve 0,62); ilkinde "rahat yerleşirsin" ifadesi vardı, yani haklı. - **L9 — Depodaki `data/app.db` şeması geride:** `danisman_profil` ve `report_locks` tabloları yoktu; `pnpm db:migrate` 0004-0005'i uyguladı. Göç koşmadan `/api/soru` POST 500 verir (koddan çıkarım). Üretimde giriş betiği göçü koşuyor. ## 3. Doğrulanan, çalışan davranışlar | Konu | Sonuç | |---|---| | Girişsiz `GET/POST/DELETE /api/soru`, `/api/soru/profil`, sahte çerez | 401 | | Kritik sayfalar (`/`, `/giris`, `/paket`, `/rehber`, `/universiteler`, `/universiteler/izmir`, `/bolum/hukuk`, `/universite/ege-universitesi`, `/meraklisina`, `/gizlilik`, `/kosullar`, `/sitemap.xml`, `/robots.txt`) | 200; `/listem` girişsizde `/giris?callback=%2Flistem` yönlendirmesi; olmayan sayfa 404 | | Listesiz kullanıcı | 409 `NO_REPORT`, kredi düşmüyor | | Kredi düşümü | her soruda −1, defterde `chat_message`; onay "evet" → `X-Kredi-Dusuldu: 0` | | Kredi 0 | 402 `INSUFFICIENT`, mesaj yazılmıyor, `kredi_bitti_at` işaretleniyor | | Aynı `clientMessageId` | 409 `DUPLICATE`, kredi düşmüyor | | Girdi doğrulama (boş, 2001 karakter, sayı tipinde mesaj, kimliksiz, dizi gövde) | 400 | | Sağlayıcı 500 / 429 (3 deneme) | akışta hata parçası, kredi iade (`refund`) | | Akış yarıda kopunca | yarım metin kaydedildi, kredi iade | | Boş cevap | hata, kredi iade | | Araç döngüsü | 4 adımda duruyor; 4. adımda `tool_choice: "none"` | | Bozuk JSON'lu araç argümanı | `tool-input-error`, cevap sürüyor | | Jev | her cevapta `[soru] jev ihlal olasılığı: …`; garanti dilli sahte cevapta 0,96 ve not hem ekranda hem kayıtta | | Kayıt–ekran tutarlılığı | akıştan birleştirilen metin kayıtla aynı (not dahil) | | Yetki | istek gövdesinden yalnız `message` ve `clientMessageId` okunuyor; liste, geçmiş, profil oturumdaki kullanıcıdan (koddan çıkarım + testte başka kullanıcı verisi dönmedi) | | `profil_guncelle` | kapalı liste: `saglik`, `userId`, `__proto__` reddedildi; serbest metin ve aralık dışı sayı reddedildi | | Paketsiz kullanıcı | danışman kilitli 4-5. tercihleri "görmüyorum" dedi | Gerçek model çağrısı: 13 adım (sınır 15). Sahte sağlayıcılı testler model harcamadı. ## 4. Test edilemeyenler - **Tarayıcıdaki sohbet arayüzü** (araç etiketi satırı, toast metinleri, kopma sonrası toparlama, bakiye rozeti): AGENTS.md gereği tarayıcı açılmadı. M1'in istemci yarısı koddan çıkarım. - **Jev zaman aşımı ve anahtarsız yol:** çalıştırılmadı; `jev.ts:62-71` koddan okununca cevap olduğu gibi kalıyor. - **Eşzamanlı çift istek** (aynı kullanıcı, iki sekme): denenmedi. - **Paketli kullanıcı** (24 satırın tamamı prompt'ta): denenmedi. - **Gerçek sağlayıcıda zaman aşımı:** yalnız sahte sağlayıcıyla üretildi. ## 5. Araçlar ve sohbet akışı Next dışından nasıl koşulur Engel: `src/lib/katalog.ts`, `rehber.ts`, `danisman-profil-db.ts` `import "server-only"` içeriyor; düz `tsx` altında paket fırlatıyor. Çözüm: `react-server` koşulu — `server-only` bu koşulda boş modüle çözülür. Kod değişikliği gerekmez. ```sh # çalışma dizini worktree kökü olmalı (data/yokatlas.db ve file:./data/app.db buna göre çözülür) pnpm exec tsx --conditions=react-server betik.ts # yalnız araçlar pnpm exec tsx --env-file=.env.local --conditions=react-server betik.ts # sohbet akışı (model + Jev anahtarı) ``` - **Araçlar:** `aracCalistirici(baglam)(ad, arg)` — `src/lib/ai/araclar.ts:812`. Bağlam: `{ userId, sira, tur, profil: {} }`. 10 araç yalnız `yokatlas.db` okur; `profil_guncelle` `app.db`'ye yazar ve `user` tablosunda o `userId` olmalı (yabancı anahtar). AI SDK sarmalayıcısı `aracSeti(baglam)` aynı gövdeleri kullanır. - **Sohbet akışı:** `danismanAkisi({ system, mesajlar, aracBaglami, onTamam, onYarim })` — `src/lib/ai/sohbet-akisi.ts:29`. `ReadableStream` döner; `text-delta`, `tool-input-available`, `tool-output-available` parçaları okunur. Sistem prompt'u `route.ts` içinde dışa aktarılmamış (`sistemPromptu`, `:57`); eval için ya kopyalanmalı ya da dışa aktarılması istenmeli. - **`@/` yolları:** `tsx` worktree'nin `tsconfig.json`'ını kendiliğinden kullanıyor; betik worktree dışında durabilir, içe aktarmalar mutlak yolla yapılır. - **Model harcamadan hata yolu:** `AI_BASE_URL=http://127.0.0.1:3940/v1 AI_API_KEY=x` ile OpenAI uyumlu küçük bir sahte sunucu (500, yarıda kesme, takılma, sonsuz araç çağrısı) hem betikte hem `pnpm start`'ta çalışıyor. - **Lokal sunucuda giriş:** `RESEND_API_KEY= EPOSTA_KONSOL_FALLBACK=1 RYBBIT_HOST= BETTER_AUTH_URL=http://localhost: PORT= pnpm start` → giriş linki sunucu günlüğüne düşer, e-posta ve analitik dışarı çıkmaz. Kullanılan betikler (commit edilmedi): scratchpad `qa/arac-test.ts` (118 vaka), `qa/akis-test.ts`, `qa/mock.mjs`, `qa/sor.sh`. ## 6. Açılan `qa-` kayıtları Hepsi yalnız bu worktree'nin `data/app.db` kopyasında; ana deponun veritabanına ve canlıya yazılmadı. - Kullanıcı `qa-gece-1005@qa.invalid` (magic link ile, konsol modunda; e-posta gönderilmedi). Kredisi testte elle 1 ve 10 yapıldı. - Kullanıcı `qa-arac-kullanici` / `qa-arac@qa.invalid` (araç betiği için elle eklendi). - `reports` satırı `qa-rapor-1` (mevcut bir lokal raporun kopyası). - İki `danisman_profil` satırı, ~31 `chat_messages`, ilgili `credit_ledger` / `session` / `verification` satırları. - 0004 ve 0005 göçleri uygulandı. `data/app.db` git'te izlenen bir dosya ve artık değişmiş görünüyor — **commit edilmedi, edilmemeli**. ## Bilal'den istenen 1. H3 için karar: onay mesajı tamamen bedelsiz mi kalsın, yoksa yalnız tek kelimelik "evet/hayır" mı bedelsiz olsun (2 dk). 2. H1 + H2 + H4'ün bir sonraki tura düzeltme işi olarak alınmasına onay (1 dk).