Files
ihaleist/tests/ai-jobs.test.mjs
T

86 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});