From 0c16bbd96f0e466c3468b2f53b763571b860fe87 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 10:30:51 +0300 Subject: [PATCH] Surface shared inventory failures and isolate test database storage --- docs/quality/uygulama-durumu.md | 8 ++++++++ server/index.js | 9 ++++++--- server/kalem-store.mjs | 13 ++++++++----- tests/browser-qa.mjs | 2 +- tests/http-state.test.mjs | 2 +- tests/shared-store-recovery.test.mjs | 24 ++++++++++++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 tests/shared-store-recovery.test.mjs diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index ea27725..18f2b48 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -579,3 +579,11 @@ Hazırlık formunun hazır hesabına doğrulanmış gelecek tarih/saat kontrolü Tarih/saat ayrıştırması hatalı bitişik ekleri (ör. `10:0099`) geçerli saat kabul etmez. Mevcut Türkiye saat dilimi ve kaynak tarih doğrulaması kullanılır. Testler: son teklif anından bir milisaniye önce/sonra; bilinmeyen/geçersiz tarih ve saat; gelecekte tarih taşısa da iptal/sonuçlanmış kayıt. Tüm 127 test ve üretim derlemesi geçti. Gerçek tarayıcıda zamanı geçen uzun çıktı kabulü henüz yapılmadı. Büyük paket uyarıları sürüyor. Planın diğer işleri ve canlı yayın açık. + +### D01 / D05 — Ortak kalem deposu hata ve test ayrımı + +SQLite desteği olmayan ortamda ortak kalem deposu artık boş liste gibi görünmez; hata verir. Depo bağlantısı ancak WAL ve şema kurulumu başarılı olduktan sonra önbelleğe alınır. Başarısız açılışın bağlantısı kapatılır; sorun giderilince aynı süreçte yeniden deneme mümkündür. Kalem listeleme/arama endpointleri erişim sorununu anlaşılır 503 yanıtıyla döndürür. + +`IHALEIST_SHARED_DATA_DIR` ile ortak kalem deposu dizini yapılandırılabilir. Tarayıcı QA ve HTTP test sunucuları ortak kalem verisini kendi geçici dizinlerinde tutacak şekilde güncellendi. Önceden başlatılmış test süreçleri yeniden başlatılmadı; mevcut veri dosyaları silinmedi. Diğer ortak veri depolarının tamamı bu değişiklikle izole edilmiş sayılmaz. + +Test: bozuk gerçek SQLite dosyasıyla açılış başarısızlığı, dosyanın onarılmasından sonra aynı süreçte boş liste ve yeni kalem yazma/okuma. Tüm 128 test ve üretim derlemesi geçti. HTTP testinin yeni izole depo ayarı sonrasında dört HTTP kabul testi ayrıca geçti. Canlı sistemde yapay arıza oluşturulmadı. Büyük paket uyarıları ve planın kalan kapsamı açık; canlı yayın yapılmadı. diff --git a/server/index.js b/server/index.js index e449ed0..6c07d44 100644 --- a/server/index.js +++ b/server/index.js @@ -775,13 +775,16 @@ const server = http.createServer(async (req, res) => { if (url.pathname === '/api/kalem') { const ikn = url.searchParams.get('ikn'); if (!ikn) return json(res, 400, { success: false, error: 'ikn gerekli' }); - return json(res, 200, { success: true, ...kalemStore.kalemOku(ikn) }); + try { return json(res, 200, { success: true, ...kalemStore.kalemOku(ikn) }); } + catch { return json(res,503,{success:false,error:'Kalem deposuna erişilemiyor. Daha sonra yeniden deneyin.'}); } } if (url.pathname === '/api/kalem/ara') { const q = url.searchParams.get('q') || ''; if (q.trim().length < 2) return json(res, 400, { success: false, error: 'en az 2 karakter' }); - const sonuc = kalemStore.kalemAra(q); - return json(res, 200, { success: true, adet: sonuc.length, sonuclar: sonuc }); + try { + const sonuc = kalemStore.kalemAra(q); + return json(res, 200, { success: true, adet: sonuc.length, sonuclar: sonuc }); + } catch { return json(res,503,{success:false,error:'Kalem araması şu anda kullanılamıyor. Yeniden deneyin.'}); } } // --- Ürün araması: metni ihale DOKÜMANININ içinde arar --- // EKAP teklif cetvelini kendi tarafında indeksliyor; doküman indirmeye diff --git a/server/kalem-store.mjs b/server/kalem-store.mjs index 1c15c44..2808bc0 100644 --- a/server/kalem-store.mjs +++ b/server/kalem-store.mjs @@ -23,7 +23,7 @@ try { } const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const DATA_DIR = path.join(__dirname, 'data'); +const DATA_DIR = process.env.IHALEIST_SHARED_DATA_DIR || path.join(__dirname, 'data'); const DB_PATH = path.join(DATA_DIR, 'kalem.sqlite'); const SCHEMA = ` @@ -56,11 +56,14 @@ export function available() { function open() { if (db) return db; - if (!DatabaseSync) return null; + if (!DatabaseSync) throw new Error('Kalem deposu bu sunucuda kullanılamıyor.'); fs.mkdirSync(DATA_DIR, { recursive: true }); - db = new DatabaseSync(DB_PATH); - db.exec('PRAGMA journal_mode = WAL;'); - db.exec(SCHEMA); + const candidate = new DatabaseSync(DB_PATH); + try { + candidate.exec('PRAGMA journal_mode = WAL;'); + candidate.exec(SCHEMA); + db = candidate; + } catch (error) { candidate.close(); throw error; } return db; } diff --git a/tests/browser-qa.mjs b/tests/browser-qa.mjs index 075a3a0..759c542 100644 --- a/tests/browser-qa.mjs +++ b/tests/browser-qa.mjs @@ -12,7 +12,7 @@ const {signToken}=await import('../server/auth.mjs'); const user={email:'browser-qa@example.test',name:'Laboratuvar QA',isAdmin:false}; const token=signToken(user); const probe=netServer();await new Promise(r=>probe.listen(0,'127.0.0.1',r));const port=probe.address().port;await new Promise(r=>probe.close(r)); -const api=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{PATH:process.env.PATH,NODE_ENV:'test',AUTH_SECRET:process.env.AUTH_SECRET,IHALEIST_USER_DATA_DIR:dir,PORT:String(port),UYUM_GOMME:'0'},stdio:'ignore'}); +const api=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{PATH:process.env.PATH,NODE_ENV:'test',AUTH_SECRET:process.env.AUTH_SECRET,IHALEIST_USER_DATA_DIR:dir,IHALEIST_SHARED_DATA_DIR:join(dir,'shared'),PORT:String(port),UYUM_GOMME:'0'},stdio:'ignore'}); const entries=[['1582991','Giresun','10.09.2026','10:00'],['1534665','Kayseri','22.09.2026','11:00'],['1499061','Akdeniz','22.09.2026','10:30'],['1573235','Aksaray','09.09.2026','10:00'],['1523851','Tepecik','11.09.2026','10:00']]; const tenders=entries.map(([id,city,date,time])=>({id:'qa-'+id,ikn:'2026/'+id,baslik:'QA — '+city+' laboratuvar',kurum:city+' TEST KURUMU',sehir:city,tarih:date,saat:time,alimTuru:'Mal Alımı',sektor:'Laboratuvar & Ölçüm',sektorler:['Laboratuvar & Ölçüm'],durum:'Açık',takipte:false,kanbanStatus:'incelenecek',items:[{id:'1',ad:'Sentetik test kalemi',miktar:2,birim:'adet'}],aciklama:'Bu yalnızca yerel arayüz test verisidir. Teklif geçerlilik süresi 120 takvim günü. Birim fiyat teklif. '.repeat(3)})); const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use((req,res,next)=>{ diff --git a/tests/http-state.test.mjs b/tests/http-state.test.mjs index c82ceb1..06eb22b 100644 --- a/tests/http-state.test.mjs +++ b/tests/http-state.test.mjs @@ -19,7 +19,7 @@ const request=async(path,method='GET',body,auth=token)=>{ before(async()=>{ const socket=createServer();await new Promise(r=>socket.listen(0,'127.0.0.1',r));const port=socket.address().port;await new Promise(r=>socket.close(r)); base='http://127.0.0.1:'+port; - child=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{...process.env,NODE_ENV:'test',PORT:String(port),IHALEIST_USER_DATA_DIR:dir,UYUM_GOMME:'0'},stdio:'ignore'}); + child=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{...process.env,NODE_ENV:'test',PORT:String(port),IHALEIST_USER_DATA_DIR:dir,IHALEIST_SHARED_DATA_DIR:join(dir,'shared'),UYUM_GOMME:'0'},stdio:'ignore'}); for(let i=0;i<100;i++){try{await fetch(base+'/api/auth/google/config');return;}catch{await new Promise(r=>setTimeout(r,50));}} throw new Error('QA API did not start'); }); diff --git a/tests/shared-store-recovery.test.mjs b/tests/shared-store-recovery.test.mjs new file mode 100644 index 0000000..34d771e --- /dev/null +++ b/tests/shared-store-recovery.test.mjs @@ -0,0 +1,24 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {mkdtempSync,writeFileSync,rmSync,existsSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {execFileSync} from 'node:child_process'; +test('failed shared-store initialization is retryable after storage is repaired',()=>{ + const dir=mkdtempSync(join(tmpdir(),'ihaleist-shared-recovery-')); + try { + writeFileSync(join(dir,'kalem.sqlite'),'not a sqlite database'); + execFileSync(process.execPath,['--input-type=module','-e',` + import assert from 'node:assert/strict'; + import {rmSync} from 'node:fs'; + import {join} from 'node:path'; + const store=await import('./server/kalem-store.mjs'); + assert.throws(()=>store.kalemOku('2026/123')); + rmSync(join(process.env.IHALEIST_SHARED_DATA_DIR,'kalem.sqlite')); + assert.deepEqual(store.kalemOku('2026/123'),{kalemler:[],kaynak:null}); + assert.equal(store.kalemYaz('2026/123',[{ad:'QA tüp',miktar:2,birim:'adet'}]),1); + assert.equal(store.kalemOku('2026/123').kalemler[0].ad,'QA tüp'); + `],{cwd:new URL('..',import.meta.url),env:{...process.env,IHALEIST_SHARED_DATA_DIR:dir},stdio:'pipe'}); + assert.equal(existsSync(join(dir,'kalem.sqlite')),true); + } finally {rmSync(dir,{recursive:true,force:true});} +});