const {test}=require('node:test'); const assert=require('node:assert/strict'); const queue=require('../.test-build/conversation-store.js'); const actualQueueSocialReply=queue.queueSocialReply; const ale=require('../.test-build/ale-store.js'); require('../.test-build/community.js').communityTick=async()=>{}; const {runConversationTick}=require('../.test-build/conversation.js'); test('conversation failure, queued delivery, confirmed memory and replay safety',async()=>{ const request={id:'request-1',speakerName:'Player',message:'Hi',channel:'whisper',receiver:'Resident',nearbyBots:[]}; const bot={id:'bot-1',name:'Resident',status:'online',autonomy:{managed:true},memories:[],personality:{},race:'Orc',role:'Warrior'}; const state={revision:0,sequence:0,events:[],bots:[bot],settings:{provider:'ollama',model:'test',brains:{enabled:false,residentMemoryLimit:5,residentMaxTokens:100}}}; const store={load:async()=>state,save:async()=>{}}; let pending=request,delivery=[],queued=0,failed='',completed=0; queue.pendingDeliveries=async()=>delivery; queue.leaseConversation=async()=>{const r=pending;pending=null;return r;}; queue.failConversation=async(_db,_id,error)=>{failed=error;}; queue.completeConversation=async()=>{completed++;}; queue.cleanupConversations=async()=>{}; queue.awaitDelivery=async(_db,r,botId,reply,commandId)=>{delivery=[{request:r,botId,reply,commandId,status:'pending'}];}; ale.queueAleCommand=async()=>{queued++;return {id:'command-1'};}; const originalFetch=global.fetch; try{ await assert.rejects(runConversationTick(store,{},{}),/not configured/); assert.match(failed,/no canned reply/);assert.equal(queued,0);assert.equal(bot.memories.length,0); pending=request;global.fetch=async()=>({ok:false,status:503}); await assert.rejects(runConversationTick(store,{}, {LLM_URL:'http://model'}),/503/); assert.equal(queued,0); pending=request;global.fetch=async()=>({ok:true,json:async()=>({message:{content:'{"reply":"Hello, traveler."}'}})}); await runConversationTick(store,{}, {LLM_URL:'http://model'}); assert.equal(queued,1);assert.equal(bot.memories.length,0);assert.equal(completed,0); await runConversationTick(store,{},{}); assert.equal(bot.memories.length,0); delivery[0].status='completed'; await runConversationTick(store,{},{}); assert.equal(bot.memories.length,1);assert.equal(completed,1); await runConversationTick(store,{},{}); assert.equal(bot.memories.length,1); delivery=[{...delivery[0],request:{...request,id:'request-2'},status:'failed',error:'Bot offline'}]; await runConversationTick(store,{},{}); assert.equal(failed,'Bot offline');assert.equal(bot.memories.length,1); }finally{global.fetch=originalFetch;} }); test('social exchanges remember both residents only after confirmed reply and replays are harmless',async()=>{ const a={id:'a',name:'A',status:'online',autonomy:{managed:true},memories:[],relationships:[],personality:{},race:'Orc',role:'Warrior'}; a.world={at:Date.now()/1000,mapId:0,team:0,x:0,y:0}; const b={...a,id:'b',name:'B',memories:[],relationships:[]}; const state={revision:0,sequence:0,events:[],bots:[a,b],settings:{provider:'ollama',model:'test',brains:{enabled:false,residentMemoryLimit:5,residentMaxTokens:100}}}; const store={load:async()=>state,save:async()=>{}}; const request={id:'social-test',createdAt:new Date().toISOString(),socialTurn:0,speakerName:'B',selectedPlayer:'A',message:'Initiate',channel:'say',nearbyBots:['B']}; let pending=request,deliveries=[],followups=0,source=''; queue.pendingDeliveries=async()=>deliveries; queue.leaseConversation=async()=>{const r=pending;pending=null;return r;}; queue.cleanupConversations=async()=>{}; queue.completeConversation=async()=>{}; queue.failConversation=async()=>{}; queue.awaitDelivery=async(_db,r,botId,reply,commandId)=>{deliveries=[{request:r,botId,reply,commandId,status:'pending'}];}; queue.queueSocialReply=async(_db,r,name,reply)=>{if(r.socialTurn===0){followups++;pending={...r,id:r.id+':reply',socialTurn:1,speakerName:name,selectedPlayer:'B',message:reply};}}; ale.queueAleCommand=async(_db,s)=>{source=s;return {id:'cmd'};}; const originalFetch=global.fetch; global.fetch=async()=>({ok:true,json:async()=>({message:{content:'{"reply":"How is the hunt going?"}'}})}); try{ await runConversationTick(store,{}, {LLM_URL:'http://model'}); assert.match(source,/GetDistance\(peer\)>25/); assert.match(source,/GetTeam/); assert.equal(a.memories.length,0); deliveries[0].status='completed'; await runConversationTick(store,{}, {LLM_URL:'http://model'}); assert.equal(followups,1);assert.equal(a.memories.length,0); deliveries[0].status='completed'; await runConversationTick(store,{},{}); assert.equal(a.memories.length,1);assert.equal(b.memories.length,1); assert.equal(a.relationships[0].targetId,'b');assert.equal(b.relationships[0].targetId,'a'); assert.equal(a.relationships[0].affinity,1);assert.equal(b.relationships[0].affinity,1); await runConversationTick(store,{},{}); assert.equal(a.memories.length,1);assert.equal(b.memories.length,1);assert.equal(followups,1); }finally{global.fetch=originalFetch;} }); test('follow-ups alternate speakers, retain encounter deadline and stop at four lines or farewell',async()=>{ const rows=[];const db={prepare:()=>({bind:(...args)=>({run:async()=>{rows.push(args);}})})}; const createdAt=new Date().toISOString(); for(let turn=0;turn<4;turn++)await actualQueueSocialReply(db,{id:'social-test'+':reply'.repeat(turn),createdAt,socialTurn:turn,speakerName:turn%2?'A':'B',zoneName:'Forest'},turn%2?'B':'A','Still looking for those last two.'); assert.equal(rows.length,3); assert.deepEqual(rows.map(r=>r[4]),['social-reply','social-turn-2','social-turn-3']); assert.ok(rows.every(r=>r[1]===createdAt)); assert.deepEqual(rows.map(r=>[r[2],r[6]]),[['A','B'],['B','A'],['A','B']]); await actualQueueSocialReply(db,{socialTurn:1,createdAt},'B','See you later!'); await actualQueueSocialReply(db,{socialTurn:1,createdAt:new Date(Date.now()-121000).toISOString()},'B','Hello'); assert.equal(rows.length,3); }); test('busy social pair is rejected before spending inference',async()=>{ const bot={id:'a',name:'A',status:'online',autonomy:{managed:true},world:{at:Date.now()/1000,mapId:0,team:0,x:0,y:0,humanControlled:true}}; const state={bots:[bot,{...bot,id:'b',name:'B'}]}; queue.pendingDeliveries=async()=>[];queue.cleanupConversations=async()=>{}; queue.leaseConversation=async()=>({id:'social-busy',socialTurn:2,createdAt:new Date().toISOString(),speakerName:'B',selectedPlayer:'A',channel:'say',nearbyBots:['B']}); let error='';queue.failConversation=async(_db,_id,e)=>{error=e;}; await assert.rejects(runConversationTick({load:async()=>state},{},{}),/no longer available/); assert.match(error,/no longer available/); });