Verify durable AI job result recovery through the browser

This commit is contained in:
2026-09-06 12:37:53 +03:00
parent f830625b60
commit 801d4fca8d
4 changed files with 39 additions and 2 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"provider": "QA simulation, no paid AI call",
"checks": {
"runningVisible": true,
"duplicateCreateDisabled": true,
"completedAfterClose": true,
"restoredText": true,
"restoredType": true,
"draftWarning": true
},
"running": "\n - generic \"Kaynak değişiklik geçmişi\"\n - button \"Listeye Geri Dön\":\n - generic: Listeye Geri Dön\n - button \"İncelendi olarak işaretle\"\n - button \"Takip Et\":\n - generic: Takip Et\n - button \"🗂 Hazırlık Görevi Aç\"\n - button \"İhale detayını kapat\":\n - generic: Mal Alımı\n - generic: \"İKN: 2026/1582991\"\n - generic: \"Ekleme:\"\n - heading \"QA — Giresun laboratuvar\" [level=1]\n - generic: Kurum Adı\n - generic \"Giresun TEST KURUMU\"\n - generic: İhale Tarihi & Saat\n - generic: 10.09.2026 - 10:00\n - generic: Şehir / Bölge\n - generic: Giresun ()\n - generic: Yaklaşık Maliyet\n - generic: Gizli / Belirtilmemiş\n - button \"İhale Detayları\":\n - generic: İhale Detayları\n - button \"Birim Fiyat Cetveli (PDF & Kaşe) İnteraktif\":\n - generic: Birim Fiyat Cetveli (PDF & Kaşe)\n - generic: İnteraktif\n - button \"Yapay Zeka Asistanı\":\n - generic: Yapay Zeka Asistanı\n - button \"Hazırlık Checklist\":\n - generic: Hazırlık Checklist\n - button \"Teklif Asistanı AI\":\n - generic: Teklif Asistanı\n - generic: AI\n - button \"Belgeye Sor\":\n - text: Belgeye Sor\n - button \"Taslağı Denetle\":\n - text: Taslağı Denetle\n - button \"Taslak Üret\":\n - text: Taslak Üret\n - text: Doküman yükle (.pdf/.txt) — isteğe bağlı\n - paragraph: Metin sağlanmazsa sunucu EKAP dokümanını / ilan metnini kullanır. Yanıtların kaynak parçalarını kontrol edin; AI yanıtları hata veya eksik bilgi içerebilir.\n - generic \"Kayıtlı AI işleri\"\n - status: AI işi sunucuda işleniyor. Ekranı kapatırsanız sonucu Kayıtlı AI işleri bölümünden açabilirsiniz.\n - combobox \"Taslak türü\":\n - option \"Teklif mektubu (kapak)\" [selected]\n - option \"Teknik teklif\"\n - option \"Ticari teklif\"\n - option \"Tam dosya\"\n - button \"Taslağı üret\" [disabled]:\n - text: Taslağı üret\n- button \"Kullanım kılavuzu (? tuşu)\":",
"listed": "\n - generic \"Kaynak değişiklik geçmişi\"\n - button \"Listeye Geri Dön\":\n - generic: Listeye Geri Dön\n - button \"İncelendi olarak işaretle\"\n - button \"Takip Et\":\n - generic: Takip Et\n - button \"🗂 Hazırlık Görevi Aç\"\n - button \"İhale detayını kapat\":\n - generic: Mal Alımı\n - generic: \"İKN: 2026/1582991\"\n - generic: \"Ekleme:\"\n - heading \"QA — Giresun laboratuvar\" [level=1]\n - generic: Kurum Adı\n - generic \"Giresun TEST KURUMU\"\n - generic: İhale Tarihi & Saat\n - generic: 10.09.2026 - 10:00\n - generic: Şehir / Bölge\n - generic: Giresun ()\n - generic: Yaklaşık Maliyet\n - generic: Gizli / Belirtilmemiş\n - button \"İhale Detayları\":\n - generic: İhale Detayları\n - button \"Birim Fiyat Cetveli (PDF & Kaşe) İnteraktif\":\n - generic: Birim Fiyat Cetveli (PDF & Kaşe)\n - generic: İnteraktif\n - button \"Yapay Zeka Asistanı\":\n - generic: Yapay Zeka Asistanı\n - button \"Hazırlık Checklist\":\n - generic: Hazırlık Checklist\n - button \"Teklif Asistanı AI\":\n - generic: Teklif Asistanı\n - generic: AI\n - button \"Belgeye Sor\":\n - text: Belgeye Sor\n - button \"Taslağı Denetle\":\n - text: Taslağı Denetle\n - button \"Taslak Üret\":\n - text: Taslak Üret\n - text: Doküman yükle (.pdf/.txt) — isteğe bağlı\n - paragraph: Metin sağlanmazsa sunucu EKAP dokümanını / ilan metnini kullanır. Yanıtların kaynak parçalarını kontrol edin; AI yanıtları hata veya eksik bilgi içerebilir.\n - generic \"Kayıtlı AI işleri\" [active]\n - paragraph: Ekran kapansa da sunucudaki işler devam eder. Son 50 iş gösterilir. Kesilen işi yeniden başlatmak sağlayıcıya yeni istek gönderir. Eski sonucu açmak asistanın mevcut metinlerini değiştirir.\n - button \"İş listesini yenile\"\n - list:\n - paragraph: Taslak üretimi · Tamamlandı · 06.09.2026 12:36:55\n - button \"Kaynak ve sonucu aç\"\n - textbox \"Ör. Geçici teminat oranı nedir?\"\n - button \"Sor\"\n - button \"Geçici teminat oranı ve süresi nedir?\"\n - button \"Teslim süresi kaç gün?\"\n - button \"Gecikme cezası ne kadar?\"\n - button \"Hangi belgeler istenmiş?\"\n - button \"Numune isteniyor mu?\"\n- button \"Kullanım kılavuzu (? tuşu)\":",
"restored": "\n - generic \"Kaynak değişiklik geçmişi\"\n - button \"Listeye Geri Dön\":\n - generic: Listeye Geri Dön\n - button \"İncelendi olarak işaretle\"\n - button \"Takip Et\":\n - generic: Takip Et\n - button \"🗂 Hazırlık Görevi Aç\"\n - button \"İhale detayını kapat\":\n - generic: Mal Alımı\n - generic: \"İKN: 2026/1582991\"\n - generic: \"Ekleme:\"\n - heading \"QA — Giresun laboratuvar\" [level=1]\n - generic: Kurum Adı\n - generic \"Giresun TEST KURUMU\"\n - generic: İhale Tarihi & Saat\n - generic: 10.09.2026 - 10:00\n - generic: Şehir / Bölge\n - generic: Giresun ()\n - generic: Yaklaşık Maliyet\n - generic: Gizli / Belirtilmemiş\n - button \"İhale Detayları\":\n - generic: İhale Detayları\n - button \"Birim Fiyat Cetveli (PDF & Kaşe) İnteraktif\":\n - generic: Birim Fiyat Cetveli (PDF & Kaşe)\n - generic: İnteraktif\n - button \"Yapay Zeka Asistanı\":\n - generic: Yapay Zeka Asistanı\n - button \"Hazırlık Checklist\":\n - generic: Hazırlık Checklist\n - button \"Teklif Asistanı AI\":\n - generic: Teklif Asistanı\n - generic: AI\n - button \"Belgeye Sor\":\n - text: Belgeye Sor\n - button \"Taslağı Denetle\":\n - text: Taslağı Denetle\n - button \"Taslak Üret\":\n - text: Taslak Üret\n - text: Doküman yükle (.pdf/.txt) — isteğe bağlı\n - paragraph: Metin sağlanmazsa sunucu EKAP dokümanını / ilan metnini kullanır. Yanıtların kaynak parçalarını kontrol edin; AI yanıtları hata veya eksik bilgi içerebilir.\n - generic \"Kayıtlı AI işleri\"\n - paragraph: Ekran kapansa da sunucudaki işler devam eder. Son 50 iş gösterilir. Kesilen işi yeniden başlatmak sağlayıcıya yeni istek gönderir. Eski sonucu açmak asistanın mevcut metinlerini değiştirir.\n - button \"İş listesini yenile\"\n - list:\n - paragraph: Taslak üretimi · Tamamlandı · 06.09.2026 12:36:55\n - button \"Kaynak ve sonucu aç\"\n - combobox \"Taslak türü\":\n - option \"Teklif mektubu (kapak)\" [selected]\n - option \"Teknik teklif\"\n - option \"Ticari teklif\"\n - option \"Tam dosya\"\n - button \"Yeniden üret\":\n - text: Yeniden üret\n - button \"Tümünü kopyala\":\n - text: Tümünü kopyala\n - status:\n - paragraph: Taslak — kullanıcı kontrolü gerekli\n - paragraph: Bu çıktı düzenlenebilir bir taslaktır. Bölümlerin bulunması, bilgilerin doğru veya resmi teklif dosyasının hazır olduğunu göstermez. Kaynak, firma bilgileri, fiyatlar ve zorunlu belgeler ayrıca kontrol edilmelidir.\n - textbox: Teklif Mektubu (Kapak)\n - textbox: \"QA kayıtlı sonuç: Laboratuvar teklif taslağı. Bu sentetik metin resmi teklif değildir.\"\n- button \"Kullanım kılavuzu (? tuşu)\":"
}
+8
View File
@@ -723,3 +723,11 @@ Teklif asistanının soru, taslak denetimi ve taslak üretimi kalıcı işe bağ
İstemci tek uzun HTTP isteği yerine işi oluşturur ve kısa durum istekleriyle sonucu bekler. Ekran kapanınca yalnız takip isteği kesilir; sunucu işi devam eder. Kayıtlı AI işleri bölümünde son 50 iş yenilenebilir, iptal/yeniden başlatılabilir; tamamlanmış işin kaynağı ve sonucu asistan ekranına geri açılabilir. Eski işi açmanın mevcut metni değiştirdiği belirtilir. Doğrudan eski API uçları uyumluluk için korunur. İstemci tek uzun HTTP isteği yerine işi oluşturur ve kısa durum istekleriyle sonucu bekler. Ekran kapanınca yalnız takip isteği kesilir; sunucu işi devam eder. Kayıtlı AI işleri bölümünde son 50 iş yenilenebilir, iptal/yeniden başlatılabilir; tamamlanmış işin kaynağı ve sonucu asistan ekranına geri açılabilir. Eski işi açmanın mevcut metni değiştirdiği belirtilir. Doğrudan eski API uçları uyumluluk için korunur.
Doğrulama: hesap ayrımı, idempotency, kesilen işin otomatik tekrarlanmaması, çalışma kimliği, iptal sonrası geç sonuç reddi, HTTP bağlantısından bağımsız sonuç kaydı; istemci oluşturma/poll/tamamlama ve ekran kapanınca silme çağrısı olmaması; HTTP yetki ve boş liste. Tüm 161 test ve üretim derlemesi geçti. Sağlayıcı testleri taklittir; gerçek uzun işlem/tarayıcı geri açma kabulü yapılmadı. Genel AI analiz ekranı henüz bu iş modeline bağlı değil. ai-jobs.sqlite kullanıcı yedek aracına otomatik dahil değildir; veri volume yedeğiyle ayrıca alınmalı. Canlı yayın yok. Doğrulama: hesap ayrımı, idempotency, kesilen işin otomatik tekrarlanmaması, çalışma kimliği, iptal sonrası geç sonuç reddi, HTTP bağlantısından bağımsız sonuç kaydı; istemci oluşturma/poll/tamamlama ve ekran kapanınca silme çağrısı olmaması; HTTP yetki ve boş liste. Tüm 161 test ve üretim derlemesi geçti. Sağlayıcı testleri taklittir; gerçek uzun işlem/tarayıcı geri açma kabulü yapılmadı. Genel AI analiz ekranı henüz bu iş modeline bağlı değil. ai-jobs.sqlite kullanıcı yedek aracına otomatik dahil değildir; veri volume yedeğiyle ayrıca alınmalı. Canlı yayın yok.
### D06 / D09 — Kalıcı AI işinin ekran kapatma / geri açma kabulü
localhost:5178 QA sunucusu, yalnız test ortamında ücretli sağlayıcı yerine kontrollü bekleyen iş kullanacak şekilde genişletildi. İstemcinin iş oluşturması gerçek geçici ai-jobs.sqlite kaydına gider; liste/sonuç okuması gerçek uygulama API'sinden yapılır. Görünür QA kontrol ekranı tamamlanmayı tetikler. Üretim sağlayıcı veya işi çalıştıran üretim runner bu UI senaryosunda test edilmez; runner önceki otomatik testte kapsanmıştır.
Giresun → Teklif Asistanı → Taslak Üret → Kapak → Taslağı üret akışında işleniyor durumu ve kapalı üretme düğmesi görüldü. İhale penceresi kapatıldı; kontrol ekranından iş tamamlandı. Yeniden açılışta kayıtlı iş Tamamlandı göründü. Kaynak ve sonucu aç ile doğru kapak türü, kayıtlı taslak metni ve kullanıcı kontrolü uyarısı geri geldi. Altı kabul kontrolü doğru; kanıt qa-durable-ai-job.json.
Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Önceki 161 otomatik test yalnız QA/etiket değişikliği için tekrarlanmadı. Gerçek uzun sağlayıcı, yüklenen PDF kaynağının geri açılması, kesinti/yeniden başlatma tarayıcı kabulü ve genel AI iş entegrasyonu açık. Canlı yayın yapılmadı.
+1 -1
View File
@@ -171,7 +171,7 @@ const OfferAssistantSession = ({ tender }) => {
{arac === 'uret' && ( {arac === 'uret' && (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<select value={tur} onChange={(e) => setTur(e.target.value)} className="bg-slate-50 border border-slate-200 rounded-lg px-2 py-1.5 text-sm"> <select aria-label="Taslak türü" value={tur} onChange={(e) => setTur(e.target.value)} className="bg-slate-50 border border-slate-200 rounded-lg px-2 py-1.5 text-sm">
<option value="kapak">Teklif mektubu (kapak)</option><option value="teknik">Teknik teklif</option><option value="ticari">Ticari teklif</option><option value="tam">Tam dosya</option> <option value="kapak">Teklif mektubu (kapak)</option><option value="teknik">Teknik teklif</option><option value="ticari">Ticari teklif</option><option value="tam">Tam dosya</option>
</select> </select>
<button type="button" onClick={uret} disabled={mesgul} className="bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white font-bold text-xs px-4 py-2 rounded-lg flex items-center gap-1.5"> <button type="button" onClick={uret} disabled={mesgul} className="bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white font-bold text-xs px-4 py-2 rounded-lg flex items-center gap-1.5">
+16 -1
View File
@@ -14,6 +14,11 @@ const token=signToken(user);
const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true}; const keyUser={email:'keys-browser-qa@example.test',name:'API QA',isAdmin:true};
const keyToken=signToken(keyUser); const keyToken=signToken(keyUser);
let apiKeyListFailure=false; let apiKeyListFailure=false;
process.env.IHALEIST_USER_DATA_DIR=dir;
const qaJobs=await import('../server/ai-job-store.mjs');
const {qualifyOfferDraft}=await import('../server/offer-draft.mjs');
const pendingQaJobs=new Map();
const qaHeartbeat=setInterval(()=>{for(const job of pendingQaJobs.values())qaJobs.heartbeatAiJob(job.id,job.runId);},5000);qaHeartbeat.unref();
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 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,IHALEIST_SHARED_DATA_DIR:join(dir,'shared'),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 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']];
@@ -24,6 +29,16 @@ const {observeTenders}=await import('../server/tender-history.mjs');
observeTenders(tenders); observeTenders(tenders);
for(let revision=1;revision<=22;revision++) observeTenders([{...tenders[0],saat:revision%2?'11:00':'10:00'}]); for(let revision=1;revision<=22;revision++) observeTenders([{...tenders[0],saat:revision%2?'11:00':'10:00'}]);
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)=>{ 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)=>{
if(req.url==='/qa-ai-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel AI işi testi</h1><p>Sağlayıcı taklidi kullanılır; ücretli AI çağrısı yapılmaz.</p><button id="finish">Bekleyen QA işini tamamla</button><p role="status"></p><script>document.querySelector('#finish').onclick=async()=>{const r=await fetch('/qa-ai-complete',{method:'POST'});document.querySelector('[role=status]').textContent=await r.text();};</script></html>`);return;}
if(req.url==='/qa-ai-complete' && req.method==='POST') {for(const job of pendingQaJobs.values())qaJobs.finishAiJob(job.id,job.runId,{success:true,...qualifyOfferDraft({bolumler:[{baslik:'Teklif Mektubu (Kapak)',icerik:'QA kayıtlı sonuç: Laboratuvar teklif taslağı. Bu sentetik metin resmi teklif değildir.'}]},['Teklif Mektubu (Kapak)'])});pendingQaJobs.clear();res.end('QA işleri tamamlandı');return;}
if(req.url==='/api/me/ai-jobs' && req.method==='POST') {
if(req.headers.authorization!=='Bearer '+token){res.writeHead(401);res.end('{}');return;}
let body='';req.on('data',chunk=>body+=chunk);req.on('end',()=>{try{
const input=JSON.parse(body);const created=qaJobs.createAiJob(user.email,input.requestId,input.operation,input.input);
const claimed=qaJobs.claimAiJob();if(claimed)pendingQaJobs.set(claimed.id,claimed);
res.setHeader('Content-Type','application/json');res.end(JSON.stringify({success:true,job:qaJobs.getAiJob(user.email,created.id)}));
}catch(error){res.writeHead(400,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:error.message}));}});return;
}
if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;} if(req.url==='/qa-api-controls') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel API testi</h1><p>Yalnız geçici test deposunu etkiler.</p><button id="login">API test hesabıyla gir</button><button id="fail">Liste hatasını aç</button><button id="recover">Liste hatasını kapat</button><p role="status"></p><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(keyToken)},${JSON.stringify(keyUser)});location.href='/';};for(const [id,mode] of [['fail','on'],['recover','off']])document.getElementById(id).onclick=async()=>{await fetch('/qa-api-failure?mode='+mode,{method:'POST'});document.querySelector('[role=status]').textContent=mode==='on'?'Liste hatası açık':'Liste hatası kapalı';};</script></html>`);return;}
if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;} if(req.method==='POST' && req.url?.startsWith('/qa-api-failure?')) {apiKeyListFailure=new URL(req.url,'http://localhost').searchParams.get('mode')==='on';res.end('ok');return;}
if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;} if(apiKeyListFailure && req.method==='GET' && req.url==='/api/me/apikeys') {res.writeHead(503,{'Content-Type':'application/json'});res.end(JSON.stringify({success:false,error:'QA: Anahtar deposuna erişilemiyor.'}));return;}
@@ -32,5 +47,5 @@ const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{targ
next(); next();
});}}]}); });}}]});
await ui.listen();console.log('QA_URL='+ui.resolvedUrls.local[0]+'qa-signin'); await ui.listen();console.log('QA_URL='+ui.resolvedUrls.local[0]+'qa-signin');
async function stop(){await ui.close();api.kill();rmSync(dir,{recursive:true,force:true});process.exit();} async function stop(){clearInterval(qaHeartbeat);await ui.close();api.kill();rmSync(dir,{recursive:true,force:true});process.exit();}
process.on('SIGINT',stop);process.on('SIGTERM',stop); process.on('SIGINT',stop);process.on('SIGTERM',stop);