import test,{after} from 'node:test'; import assert from 'node:assert/strict'; import {mkdtempSync,rmSync} from 'node:fs'; import {join} from 'node:path'; import {tmpdir} from 'node:os'; const dir=mkdtempSync(join(tmpdir(),'ihaleist-jobs-'));process.env.IHALEIST_USER_DATA_DIR=dir; const jobs=await import('../server/ai-job-store.mjs'); after(()=>rmSync(dir,{recursive:true,force:true})); test('durable jobs are idempotent and account scoped',()=>{ const first=jobs.createAiJob('a','request-1','teklif-denetim',{ikn:'2026/1',taslak:'private'}); assert.equal(jobs.createAiJob('a','request-1','teklif-denetim',{ikn:'2026/1',taslak:'private'}).id,first.id); assert.throws(()=>jobs.createAiJob('a','request-1','teklif-denetim',{taslak:'other'}),/farklı kaynak/); assert.equal(jobs.getAiJob('b',first.id),null);assert.deepEqual(jobs.listAiJobs('b'),[]); assert.equal(jobs.cancelAiJob('b',first.id),false); const claimed=jobs.claimAiJob();assert.equal(claimed.input.taslak,'private'); assert.equal(jobs.finishAiJob(first.id,'wrong',{success:true}),false); assert.equal(jobs.finishAiJob(first.id,claimed.runId,{success:true,answer:'done'}),true); assert.equal(jobs.getAiJob('a',first.id).result.answer,'done'); assert.equal(jobs.getAiJob('a',first.id).input.taslak,'private'); assert.equal(jobs.listAiJobs('a')[0].input,undefined); }); test('lost workers become interrupted without automatic provider replay; retry and cancellation reject late results',()=>{ const first=jobs.createAiJob('a','request-2','teklif-taslak',{ikn:'2026/2'}); const claimed=jobs.claimAiJob(Date.now()-120000); assert.equal(jobs.claimAiJob(),null); assert.equal(jobs.getAiJob('a',first.id).state,'interrupted'); assert.equal(jobs.finishAiJob(first.id,claimed.runId,{success:true}),false); assert.equal(jobs.retryAiJob('a',first.id),true); const restarted=jobs.claimAiJob();assert.notEqual(restarted.runId,claimed.runId); assert.equal(jobs.cancelAiJob('a',first.id),true); assert.equal(jobs.heartbeatAiJob(first.id,restarted.runId),false); assert.equal(jobs.finishAiJob(first.id,restarted.runId,{success:true}),false); assert.equal(jobs.getAiJob('a',first.id).state,'cancelled'); }); test('runner stores results independently of an HTTP connection',async()=>{ const {startAiJobRunner}=await import('../server/ai-job-runner.mjs'); const job=jobs.createAiJob('runner','request-run','belge-sor',{ikn:'2026/8',soru:'teslim'}); let finish; const runner=startAiJobRunner(async()=>new Promise(resolve=>{finish=resolve;}),{intervalMs:100000}); try { runner.wake();assert.equal(jobs.getAiJob('runner',job.id).state,'running'); finish({success:true,cevap:'Stored answer'}); await new Promise(resolve=>setImmediate(resolve)); assert.equal(jobs.getAiJob('runner',job.id).result.cevap,'Stored answer'); } finally {runner.stop();} }); test('resolved source survives retry and preserves original idempotency and ownership',async()=>{ const {resolveAiJobInput}=await import('../server/ai-job-source.mjs'); const original={ikn:'2026/99',soru:'teslim',resolvedSource:{text:'untrusted'}}; const created=jobs.createAiJob('source-owner','request-source','belge-sor',original); const worker=jobs.claimAiJob();let calls=0; const resolved=await resolveAiJobInput(worker,async()=>{calls++;return {text:'Source A',kaynak:'ilan metni'};},jobs.pinAiJobInput,undefined,()=>['Kalem A']); assert.deepEqual(resolved.gereklilikler,['Kalem A']);assert.equal(resolved.text,'Source A');assert.equal(resolved.resolvedSource.kaynak,'ilan metni'); assert.equal(resolved.resolvedSource.hash.length,64); assert.equal(jobs.pinAiJobInput(worker.id,worker.runId,{text:'replacement'}),false); jobs.finishAiJob(worker.id,worker.runId,null,'provider failed'); assert.equal(jobs.createAiJob('source-owner','request-source','belge-sor',original).id,created.id); assert.equal(jobs.getAiJob('other-owner',created.id),null); assert.equal(jobs.retryAiJob('source-owner',created.id),true); const next=jobs.claimAiJob(); const reused=await resolveAiJobInput(next,async()=>{calls++;return {text:'Source B'};},jobs.pinAiJobInput); assert.equal(calls,1);assert.deepEqual(reused,resolved); assert.deepEqual(jobs.getAiJob('source-owner',created.id).input,resolved); jobs.cancelAiJob('source-owner',created.id); }); test('cancel during source fetch rejects late source, and empty resolution remains explicit',async()=>{ const {resolveAiJobInput}=await import('../server/ai-job-source.mjs'); const created=jobs.createAiJob('source-owner','request-cancel-source','teklif-taslak',{}); const worker=jobs.claimAiJob(); await assert.rejects(resolveAiJobInput(worker,async()=>{ jobs.cancelAiJob('source-owner',created.id);return {text:'late source'}; },jobs.pinAiJobInput),/artık etkin değil/); assert.deepEqual(jobs.getAiJob('source-owner',created.id).input,{}); const empty=jobs.createAiJob('source-owner','request-empty-source','teklif-taslak',{}); const emptyWorker=jobs.claimAiJob(); const input=await resolveAiJobInput(emptyWorker,async()=>({text:'',kaynak:null}),jobs.pinAiJobInput); assert.equal(input.resolvedSource.text,'');assert.equal(input.resolvedSource.kaynak,null); jobs.finishAiJob(empty.id,emptyWorker.runId,null,'failed');jobs.retryAiJob('source-owner',empty.id); const retry=jobs.claimAiJob(); assert.deepEqual(await resolveAiJobInput(retry,()=>assert.fail('must not refetch'),jobs.pinAiJobInput),input); jobs.cancelAiJob('source-owner',empty.id); });