94 lines
5.7 KiB
JavaScript
94 lines
5.7 KiB
JavaScript
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);
|
||
});
|
||
|
||
test('general analysis jobs are indexed by tender for recovery',()=>{
|
||
const job=jobs.createAiJob('analysis-owner','request-analysis','analiz',{tender:{ikn:'2026/777'},text:'Analiz kaynağı '.repeat(5)});
|
||
assert.equal(job.ikn,'2026/777');
|
||
assert.equal(jobs.listAiJobs('analysis-owner','2026/777')[0].id,job.id);
|
||
assert.equal(jobs.listAiJobs('analysis-owner','2026/778').length,0);
|
||
jobs.cancelAiJob('analysis-owner',job.id);
|
||
});
|