/** * Danışman akışındaki sunucu denetimlerinin MEKANİK testi — parasız, anahtarsız. * * pnpm tsx --conditions=react-server scripts/eval/akis-denetim.ts * * Yerelde sahte bir OpenAI-uyumlu uç açılır; "model" senaryodaki metni ya da * araç çağrılarını aynen döner. Gerçek danismanAkisi + gerçek araçlar koşar * (profil_guncelle data/app.db'nin geçici kopyasına yazar). Doğrulananlar: * vaat regex'i notu (Jev anahtarı yokken), "kaydettim" deyip kaydetmeme notu, * araç çağrısı kotası, rehber bağlantısı doğrulama (uydurma slug akışta * gerçek slug'a döner, kayıt da düzeltilmiş metni alır), koda bağlı revizyon * (9 Eki 2026: koddan işlem ekrana tool-liste_revize parçası ve modele * "sunucu işlemi" bölümü olarak gider, "yeniden kurdum" o zaman beyan * sayılmaz; işlem yokken beyan → not + kredi iadesi; koddan form → model * araçsız; liste_revize modelin araç listesinde yok). Model davranışını * ÖLÇMEZ; onu eval:sohbet ve eval:revizyon ölçer. * Beklenti tutmazsa çıkış kodu 1. */ import { createServer } from "node:http"; import { copyFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number; baglantiDuzeltme: number; revizeNotu?: boolean }; type Senaryo = { ad: string; beklenen: Beklenen; adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[]; /** Kaydedilen metinde olması / olmaması gerekenler (rehber bağlantısı senaryoları). */ metinde?: { var?: string[]; yok?: string[] }; /** Koddan işlem (route'un revizyon-niyeti sonucuna göre verdiği). */ islem?: { ad: "liste_revize" | "soru_sor"; cikti: unknown; anlatim?: { ozet: string[]; ilkN: boolean } }; /** Sahte uca giden İLK isteğin denetimi: sistem metninde olan, araç listesi, tool_choice. */ istek?: { sistemde?: string[]; sistemdeYok?: string[]; aracYok?: string[]; toolChoice?: string }; /** Ekrana giden araç parçaları (tool-input-available toolName:durum). */ parcalar?: string[]; /** onRevizeBeyani çağrıldı mı (kredi iadesi yolu). */ iadeIstendi?: boolean; /** Sahte uca giden istek sayısı (koddan cevapta 0). */ modelCagrisi?: number; }; const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0, baglantiDuzeltme = 0, revizeNotu = false): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac, baglantiDuzeltme, revizeNotu }); const REVIZYON_OK = { ok: true, toplam: 24, dagilim: { hayal: 5, dengeli: 13, garanti: 6 }, ilkTercihler: ["Bilgisayar Mühendisliği — EGE ÜNİVERSİTESİ (İZMİR)"], not: "Liste ekranda yenilendi." }; const FORM = { soruldu: true, anket: { id: "f1", sorular: [{ ad: "il", soru: "Hangi il?", secenekler: [{ deger: "izmir", etiket: "İzmir" }, { deger: "ankara", etiket: "Ankara" }] }] }, not: "Form adaya gösterildi." }; const SENARYOLAR: Senaryo[] = [ { ad: "kaydettim-aracsiz", beklenen: B(false, true), adimlar: [{ metin: "Kaydettim: Vakıfta yıllık ücret üst sınırı 400.000 TL. Aramalar buna göre süzülecek." }] }, { ad: "kaydettim-aracli", beklenen: B(false, false), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "ucretUst", deger: 400000 }) }] }, { metin: "Kaydettim: yıllık ücret en fazla 400.000 TL." }] }, { ad: "kaydettim-arac-hata", beklenen: B(false, true), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "maxKm", deger: 30 }) }] }, { metin: "Kaydettim: evden en fazla 30 km." }] }, { ad: "kaydetmedim-not-ettim", beklenen: B(false, false), adimlar: [{ metin: "Tamam, kaydetmedim. İzmir'de oturduğunu not ettim; listen zaten İzmir ağırlıklı." }] }, { ad: "vaat", beklenen: B(true, false), adimlar: [{ metin: "Bu programa yerleşme olasılığın düşük ama yine de yazabilirsin." }] }, { ad: "temiz", beklenen: B(false, false), adimlar: [{ metin: "Bu program son yerleştirmede 178.278. sırada kapandı; etiketi Riskli. Garanti verilemez." }] }, { ad: "kota", beklenen: B(false, false, 2), adimlar: [{ araclar: Array.from({ length: 6 }, (_, i) => ({ name: "il_ozeti", arguments: JSON.stringify({ il: ["İzmir", "Ankara", "Bursa", "Konya", "Adana", "Samsun"][i] }) })) }, { metin: "İllere baktım." }] }, // Model araç çağırmadan ezberden iki yanlış rehber adresi yazar; ikisi de akışta gerçek slug'a döner { ad: "rehber-uydurma", beklenen: B(false, false, 0, 2), adimlar: [{ metin: "Şu yazılara bak: [OBP](/rehber/obp-nedir) ve [Yatay geçiş](/rehber/yatay-gecis-rehberi). İkisi de sana göre." }], metinde: { var: ["/rehber/obp-nedir-nasil-hesaplanir", "/rehber/yatay-gecis-cap-yandal"], yok: ["/rehber/obp-nedir)", "/rehber/yatay-gecis-rehberi"] }, }, { ad: "rehber-gercek", beklenen: B(false, false, 0, 0), adimlar: [{ metin: "Bak: [Devlet mi vakıf mı](/rehber/devlet-mi-vakif-mi) yazısı bunu anlatıyor." }], metinde: { var: ["/rehber/devlet-mi-vakif-mi"] }, }, // Koda bağlı revizyon (QA B1): niyet var + kural geçerli → işlem ve cevap // koddan (model çağrılmaz); ekranda tool-liste_revize parçası; sayılar ve // adlar sunucu sonucundan şablonla. { ad: "koddan-revizyon", beklenen: B(false, false), islem: { ad: "liste_revize", cikti: REVIZYON_OK, anlatim: { ozet: ["ilk 5 tercih: il İZMİR"], ilkN: true } }, modelCagrisi: 0, parcalar: ["liste_revize:output-available"], iadeIstendi: false, adimlar: [{ metin: "(model çağrılmamalı)" }], metinde: { var: ["Listeni yeniden kurdum — ilk 5 tercih: il İzmir. 24 tercih: 5 hayal / 13 dengeli / 6 güvenli.", "1. Bilgisayar Mühendisliği — Ege Üniversitesi (İzmir)", "dilim sırası bilerek bozulur"], yok: ["YENİDEN KURULMADI", "model çağrılmamalı"] }, }, // Niyet regex'i eşleşti ama çıkarım "diger" dedi → işlem yok, normal sohbet; // model yine de "yeniden kurdum" derse not + kredi iadesi (güvenlik ağı). { ad: "niyet-diger-sahte-beyan", beklenen: B(false, false, 0, 0, true), istek: { sistemdeYok: ["BU TURDA SUNUCU İŞLEMİ"], aracYok: ["liste_revize"] }, parcalar: [], iadeIstendi: true, adimlar: [{ metin: "Listeni yeniden kurdum, artık ilk 5 tercihin İzmir'den." }], metinde: { var: ["YENİDEN KURULMADI", "kredisi iade edildi"] }, }, // Niyet var, kural eksik → form koddan; cevap da koddan (model çağrısı yok) { ad: "koddan-form", beklenen: B(false, false), islem: { ad: "soru_sor", cikti: FORM }, modelCagrisi: 0, parcalar: ["soru_sor:output-available"], iadeIstendi: false, adimlar: [{ metin: "(model çağrılmamalı)" }], metinde: { var: ["Birkaç kısa soru hazırladım"], yok: ["model çağrılmamalı"] }, }, // Koddan revizyon başarısız: ekrana hata parçası, cevap koddan nedeniyle (model yok) { ad: "koddan-revizyon-hata", beklenen: B(false, false), islem: { ad: "liste_revize", cikti: { hata: "Liste şu anda zaten yeniden kuruluyor.", not: "'yeniden kurdum' deme." } }, modelCagrisi: 0, parcalar: ["liste_revize:output-error"], iadeIstendi: false, adimlar: [{ metin: "(model çağrılmamalı)" }], metinde: { var: ["Listeni yeniden kuramadım: Liste şu anda zaten yeniden kuruluyor."], yok: ["model çağrılmamalı"] }, }, ]; const istekler: { system: string; araclar: string[]; toolChoice: unknown }[] = []; let aktif: Senaryo = SENARYOLAR[0]; let adimNo = 0; const sunucu = createServer((req, res) => { let govde = ""; req.on("data", (c) => (govde += c)); req.on("end", () => { try { const j = JSON.parse(govde) as { messages?: { role: string; content: string }[]; tools?: { function?: { name: string } }[]; tool_choice?: unknown }; istekler.push({ system: j.messages?.find((m) => m.role === "system")?.content ?? "", araclar: (j.tools ?? []).map((t) => t.function?.name ?? "?"), toolChoice: j.tool_choice ?? null }); } catch {} const adim = aktif.adimlar[Math.min(adimNo++, aktif.adimlar.length - 1)]; res.writeHead(200, { "Content-Type": "text/event-stream" }); const yaz = (delta: unknown, finish: string | null = null) => res.write(`data: ${JSON.stringify({ id: "x", object: "chat.completion.chunk", created: 1, model: "sahte", choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`); if ("metin" in adim) { yaz({ role: "assistant", content: adim.metin }); yaz({}, "stop"); } else { yaz({ role: "assistant", content: null, tool_calls: adim.araclar.map((a, i) => ({ index: i, id: `c${adimNo}_${i}`, type: "function", function: a })) }); yaz({}, "tool_calls"); } res.write("data: [DONE]\n\n"); res.end(); }); }); async function main() { await new Promise((r) => sunucu.listen(0, "127.0.0.1", r)); const port = (sunucu.address() as { port: number }).port; process.env.AI_BASE_URL = `http://127.0.0.1:${port}/v1`; process.env.AI_API_KEY = "sahte-anahtar"; delete process.env.TYPESAFE_API_KEY; // Jev'e gitme: yalnız regex yolu const db = `${process.env.TMPDIR ?? "/tmp"}/kolaytercih-akis-dene-${process.pid}.db`; copyFileSync("data/app.db", db); process.env.APP_DB_URL = `file:${db}`; execFileSync("node", ["scripts/db-goc.mjs"], { env: { ...process.env }, stdio: "ignore" }); const Database = (await import("better-sqlite3")).default; const sqlite = new Database(db); const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi"); // Gerçek content/rehber listesi: bağlantı doğrulama tamponu aktif const { getRehberAramaKayitlari } = await import("@/lib/rehber"); const rehberler = getRehberAramaKayitlari(); const info = console.info; console.info = () => {}; let hata = 0; for (const s of SENARYOLAR) { aktif = s; adimNo = 0; const userId = `akis-dene-${s.ad}`; sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Dene', ?, 1, 0, 1, 1, 1)").run(userId, `${userId}@eval.invalid`); let tam: string | null = null; let denetim: Beklenen | null = null; let iadeIstendi = false; const araclar: string[] = []; const parcalar: string[] = []; const parcaAdi = new Map(); istekler.length = 0; const okuyucu = danismanAkisi({ system: "test", mesajlar: [{ role: "user", content: "test" }], aracBaglami: { userId, sira: 100000, tur: "say", profil: {} }, rehberler, islem: s.islem ? { ad: s.islem.ad, girdi: {}, calistir: async () => s.islem!.cikti, anlatim: s.islem.anlatim } : undefined, onRevizeBeyani: async () => { iadeIstendi = true; return true; }, onTamam: async (m) => { tam = m; }, onYarim: async () => {}, onDenetim: (d) => { denetim = d; }, }).getReader(); for (;;) { const { done, value } = await okuyucu.read(); if (done) break; const v = value as { type: string; output?: unknown; toolCallId?: string; toolName?: string }; if (v.type === "tool-input-available" && v.toolCallId) parcaAdi.set(v.toolCallId, v.toolName ?? "?"); if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70)); if ((v.type === "tool-output-available" || v.type === "tool-output-error") && v.toolCallId?.startsWith("kod-")) parcalar.push(`${parcaAdi.get(v.toolCallId)}:${v.type.slice("tool-".length)}`); } const d = denetim as Beklenen | null; const kayit = String(tam); const metinTuttu = (s.metinde?.var ?? []).every((m) => kayit.includes(m)) && (s.metinde?.yok ?? []).every((m) => !kayit.includes(m)); const ilk = istekler[0]; const istekTuttu = !s.istek || (ilk != null && (s.istek.sistemde ?? []).every((m) => ilk.system.includes(m)) && (s.istek.sistemdeYok ?? []).every((m) => !ilk.system.includes(m)) && (s.istek.aracYok ?? []).every((a) => !ilk.araclar.includes(a)) && (s.istek.toolChoice === undefined || ilk.toolChoice === s.istek.toolChoice)); const parcaTuttu = !s.parcalar || JSON.stringify(parcalar) === JSON.stringify(s.parcalar); const iadeTuttu = s.iadeIstendi === undefined || iadeIstendi === s.iadeIstendi; const cagriTuttu = s.modelCagrisi === undefined || istekler.length === s.modelCagrisi; const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac && d.baglantiDuzeltme === s.beklenen.baglantiDuzeltme && d.revizeNotu === s.beklenen.revizeNotu && metinTuttu && istekTuttu && parcaTuttu && iadeTuttu && cagriTuttu; if (!tuttu) hata++; info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}${s.istek ? `\n istek: ${istekTuttu ? "ok" : "BEKLENENDEN FARKLI"} (araçlar ${ilk?.araclar.length ?? "?"}, tool_choice ${JSON.stringify(ilk?.toolChoice)})` : ""}${s.parcalar ? `\n koddan parçalar: ${parcalar.join(", ") || "—"}${parcaTuttu ? "" : " (BEKLENENDEN FARKLI)"}` : ""}${s.iadeIstendi !== undefined ? `\n iade istendi: ${iadeIstendi}` : ""}${s.modelCagrisi !== undefined ? `\n model çağrısı: ${istekler.length}` : ""}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`); } sqlite.close(); sunucu.close(); info(`\n${SENARYOLAR.length - hata}/${SENARYOLAR.length} senaryo geçti (gerçek modelle koşulmadı; mekanik test)`); process.exit(hata ? 1 : 0); } main().catch((e) => { console.error(e); process.exit(1); });