Yeşil Yanan Testlerin Yanılsaması
İki otonom yapay zekâ alt ajanının görevlerini tamamlayıp, çalışmalarını ayrı worktree dallarına commit’leyip main dalında birleştirmesi ve yerelde %100 geçen test paketinin bir anda kırmızı hatalarla patlaması kadar can sıkıcı çok az şey vardır.
citation-manager projesinde çok-ajanlı iş akışımız klasik bir dağıtık sistem tuzağına düştü: paylaşılan değişken durum (shared mutable persistence).
Arka uç test paketi disk üzerindeki kalıcı bir SQLite dosyası (citation_manager.db) üzerinde çalışıyordu. Ajan A kullanıcı kaydını sabit e-postalarla test ederken, eşzamanlı olarak çalışan Ajan B alıntı tekilleştirmesini veya yönetici yetkilendirme taramalarını test ettiğinde; testler birincil anahtarlar, yabancı anahtar kısıtlamaları ve artık oturum çerezleri yüzünden birbirine girdi.
Testler tek tek --test-name-pattern ile çalıştırıldığında sorunsuz geçiyor, ancak tüm paket paralel koşulduğunda deterministik olmayan bir yazı-tura oyununa dönüşüyordu.
İşte durum sızıntılarını kökünden kazıdığımız, katı negatif Rol Tabanlı Erişim Kontrolünü (RBAC) devreye aldığımız ve 25’ten fazla useState kancasını atomik bir React Context yapısına taşıdığımız sürecin detayları.
1. :memory: SQLite Sıfırlama Değişmezi
Sağlam otomatik testlerin ilk kuralı: hiçbir test, kendisinden önce başka bir testin doğduğunu bilmemelidir.
Bun/Node SQLite’ı :memory: moduna geçirmek kolaydır; asıl zor olan, hızlı test yaşam döngülerinde derlenmiş sorgu (prepared statement) kollarını sızdırmadan veya kilitlenmelere yol açmadan temiz bir izolasyonu korumaktır.
server/db.ts dosyasını dinamik veritabanı bağlama ve tavizsiz bir resetDB() fonksiyonunu destekleyecek şekilde yeniden yapılandırdık:
// server/db.ts
import { Database } from "bun:sqlite";
let dbInstance: Database | null = null;
export function getDB(path: string = process.env.DB_PATH || "citation_manager.db"): Database {
if (!dbInstance) {
dbInstance = new Database(path, { create: true });
initDB(dbInstance);
}
return dbInstance;
}
export function resetDB(): void {
if (!dbInstance) return;
// Temiz bir silme işlemi için yabancı anahtarları geçici olarak kapat
dbInstance.exec("PRAGMA foreign_keys = OFF;");
const tables = dbInstance
.query<{ name: string }, []>(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';"
)
.all();
for (const { name } of tables) {
dbInstance.exec(`DELETE FROM "${name}";`);
}
dbInstance.exec("PRAGMA foreign_keys = ON;");
}
Test koşucusunda (server/tests/api.test.ts), beforeEach kancası her tabloyu mutlak sıfıra çekerek, bir test ister tek bir alıntı ister on bin akademisyen profili oluştursun, bir sonraki testin tertemiz bir tuvalde başlamasını garanti eder.
beforeEach(() => {
resetDB();
// Deterministik temel referans verilerini yeniden tohumla
seedCoreTestFixtures();
});
2. Negatif RBAC: Açılmaması Gereken Kapıları Test Etmek
Ajanlar tarafından üretilen çoğu test ölümcül bir varsayımda bulunur: yalnızca mutlu patikayı (happy path) test ederler.
Bir yönetici oluşturur, yönetici belirteciyle (token) giriş yapar, yönetici uç noktasının 200 OK döndüğünü doğrular ve zafer ilan ederler. Peki ya şunlar olduğunda ne olur:
- Standart bir araştırmacı belirteci
/api/admin/audit-logsadresine istek attığında? - Kimliği doğrulanmamış bir istek bir alıntıyı silmeye çalıştığında?
role: "admin"hakları değiştirilmiş ancak HMAC imzası geçersiz sahte bir JWT kapıya dayandığında?
Her korumalı rota için katı negatif doğrulama paketleri ekledik. Bir güvenlik kapısı, yalnızca reddetme davranışının sağlamlığı kadar güçlüdür:
describe("RBAC & Güvenlik Çevresi", () => {
it("yönetici rotalarındaki standart kullanıcı belirteçlerini 403 Forbidden ile kesin olarak reddeder", async () => {
const userSession = await registerTestUser({ role: "member" });
const res = await app.request("/api/admin/domains", {
method: "POST",
headers: {
Authorization: `Bearer ${userSession.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ domain: "malicious-domain.edu" }),
});
expect(res.status).toBe(403);
const body = await res.json();
expect(body.error).toMatch(/admin privileges required/i);
});
it("tahrif edilmiş rol iddialarına sahip sahte belirteçleri reddeder", async () => {
// Geçersiz anahtarla imzalanmış sahte belirteç
const forgedToken = signBogusToken({ id: 1, role: "admin" });
const res = await app.request("/api/admin/users", {
headers: { Authorization: `Bearer ${forgedToken}` },
});
expect(res.status).toBe(401);
});
});
Bir uç noktanın yetkisiz istemcileri tam hata sözleşmesiyle kapı dışarı ettiğini kanıtlayan açık bir negatif doğrulaması yoksa, test paketi o rotayı korumasız kabul eder.
3. Ön Yüz: AuthContext ile 25’ten Fazla Prop’u Yok Etmek
Ön yüzde App.tsx, 25’ten fazla bağımsız useState kancası taşıyan 800 satırlık bir deve dönüşmüştü. Kimlik doğrulama durumu, kullanıcı profilleri, oturum belirteçleri ve modal bayrakları dört katmanlı alt bileşenlere elle prop olarak aktarılıyordu (prop drilling).
Bir ajan AdminDashboardPage.tsx içerisindeki bir modal hatasını düzeltmeye kalktığında, beş ara bileşenin arayüz imzasını değiştirmek zorunda kalıyordu.
Bu prop yayılmasını temizleyerek kapsüllenmiş bir AuthContext oluşturduk:
// client/src/context/AuthContext.tsx
interface AuthContextType {
user: User | null;
token: string | null;
login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
isAdmin: boolean;
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem("auth_token"));
const logout = useCallback(() => {
localStorage.removeItem("auth_token");
setToken(null);
setUser(null);
}, []);
const isAdmin = useMemo(() => user?.role === "admin", [user]);
return (
<AuthContext.Provider value={{ user, token, login, logout, isAuthenticated: !!token, isAdmin }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth, AuthProvider içinde kullanılmalıdır");
return ctx;
};
Bunu client/src/context/__tests__/AuthContext.test.tsx ve client/src/pages/__tests__/AdminDashboardPage.test.tsx dosyalarında Vitest + React Testing Library entegrasyon testleriyle destekledik; localStorage’dan belirteç hidrasyonunu, 401 yanıtlarında otomatik oturum kapatmayı ve arayüzdeki katı RBAC render mantığını doğruladık.
4. Alıntı Motoru: TeX ve HTML Kaçış Sınavı
Akademik alıntılar, basit metin birleştirmelerine karşı son derece kırılgan yapılardır. 100% Efficiency in C&A Systems: The $5 Solution başlıklı bir makale şu sistemleri anında çökertir:
- BibTeX ayrıştırıcıları (kaçış karakteri eklenmemiş
%,&ve$). - HTML meta etiketleri (OpenGraph/Dublin Core başlıklarındaki kaçışsız tırnaklar ve açılı ayraçlar).
- APA/MLA et al. kuralları (değişken yazar sayıları ve noktalama yerleşimi).
server/formatter.ts içinde temizleyicileri enjeksiyon ve sözdizimi bozulmalarına karşı güçlendirdik:
export function sanitizeLatex(str?: string | null): string {
if (!str) return "";
return str
.replace(/\\/g, "\\textbackslash{}")
.replace(/([%&$#_{}])/g, "\\$1")
.replace(/~/g, "\\textasciitilde{}")
.replace(/\^/g, "\\textasciicircum{}");
}
export function escapeAttr(str?: string | null): string {
if (!str) return "";
return str
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">");
}
server/tests/formatter.test.ts dosyasındaki 300+ satırlık birim testleriyle motor artık uç durumdaki yazar dizilerini ("von Neumann, John", "Plato" gibi tek isimli yazarları ve "World Health Organization" gibi kurumsal yazarları) alıntı stili sadakatinden ödün vermeden işliyor.
Çıkarılacak Dersler
Karmaşık kod tabanlarında otonom ajanlarla eş-programlama yaparken:
- Test paketlerinde asla paylaşılan disk durumuna güvenmeyin. Sıfır maliyetli sıfırlama kancalarıyla
:memory:veya tek kullanımlık ortamları zorunlu kılın. - Pozitif testler işlevselliği, negatif testler güvenliği kanıtlar. 401 ve 403 durumlarını her zaman açıkça doğrulayın.
- Durumu erkenden merkezileştirin. Prop’lar ikiden fazla bileşen sınırını aşıyorsa, alt ajanlar arayüzleri parçalamadan önce Context’e taşıyın.
- Kullanıcı girdisinin ham TeX ve ham HTML içereceğini varsayın. Erkenden kaçış karakterlerini ekleyin, tavizsiz filtreleyin.