Ground bot dialogue and add bounded conversational follow-ups
Core tests / core (push) Successful in 45s

This commit is contained in:
Codex
2026-09-24 19:23:03 +08:00
parent 132bf75f9d
commit d573ae2345
6 changed files with 56 additions and 17 deletions
+24
View File
@@ -1,6 +1,7 @@
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');
@@ -43,6 +44,7 @@ test('conversation failure, queued delivery, confirmed memory and replay safety'
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()=>{}};
@@ -75,3 +77,25 @@ test('social exchanges remember both residents only after confirmed reply and re
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/);
});
+1 -1
View File
@@ -4,7 +4,7 @@ const now=200000;
function bot(id='A'){return {id,name:id,status:'online',level:1,relationships:[],autonomy:{managed:true},world:{at:now,x:0,y:0,mapId:0,healthPct:100,stationarySince:now-700},lastDecision:{action:'quest'}};}
function state(){return {bots:[bot(),bot('B')],community:{guilds:[],feed:[]}};}
test('insight baseline does not invent historical events; detects changed facts once',()=>{const s=state();let a=observeInsights(s,undefined,now);assert.equal(a.events.length,0);s.bots[0].level=3;s.bots[1].status='dead';s.bots[1].death={verified:true,level:1,zone:'Forest'};s.bots.push(bot('C'));const b=observeInsights(s,a.cursor,now+60);assert.deepEqual(b.events.map(e=>[e.kind,e.amount]),[['levels',2],['deaths',1],['arrivals',1]]);assert.equal(observeInsights(s,b.cursor,now+120).events.length,0);});
test('report counts confirmed bot replies, unique positive pairs and same-goal guild progress',()=>{const s=state();s.community.guilds=[{id:'g',name:'Guild',goal:'Explore',progress:1,target:10}];const a=observeInsights(s,undefined,now);s.community.guilds[0].progress=4;s.bots[0].relationships=[{targetId:'B',affinity:5}];s.bots[1].relationships=[{targetId:'A',affinity:5}];s.community.feed=['social-1:reply','social-1:opener','whisper-1','social-2:reply'].map((id,i)=>({id,kind:'conversation',at:new Date((now+60)*1000).toISOString(),status:i===3?'pending':'confirmed',names:['A','B']}));const b=observeInsights(s,a.cursor,now+60);assert.equal(b.events.filter(e=>e.kind==='conversations').length,1);assert.equal(b.events.filter(e=>e.kind==='bonds').length,1);assert.equal(b.events.find(e=>e.kind==='guild').amount,3);assert.equal(observeInsights(s,b.cursor,now+120).events.length,0);});
test('report counts confirmed bot replies, unique positive pairs and same-goal guild progress',()=>{const s=state();s.community.guilds=[{id:'g',name:'Guild',goal:'Explore',progress:1,target:10}];const a=observeInsights(s,undefined,now);s.community.guilds[0].progress=4;s.bots[0].relationships=[{targetId:'B',affinity:5}];s.bots[1].relationships=[{targetId:'A',affinity:5}];s.community.feed=['social-1:reply','social-1:opener','whisper-1','social-2:reply','social-1:reply:reply','social-1:reply:reply:reply'].map((id,i)=>({id,kind:'conversation',at:new Date((now+60)*1000).toISOString(),status:i===3?'pending':'confirmed',names:['A','B']}));const b=observeInsights(s,a.cursor,now+60);assert.equal(b.events.filter(e=>e.kind==='conversations').length,1);assert.equal(b.events.filter(e=>e.kind==='bonds').length,1);assert.equal(b.events.find(e=>e.kind==='guild').amount,3);assert.equal(observeInsights(s,b.cursor,now+120).events.length,0);});
test('report closes at six UTC, records partial baseline and observed totals',()=>{assert.equal(reportWindow(21600),21600);assert.equal(reportWindow(21599),21600-86400);const r=summarize([{kind:'levels',amount:3}],21600,108000,22000,5);assert.equal(r.partial,true);assert.equal(r.observedSince,22000);assert.equal(r.totals.levels,3);assert.equal(r.samples,5);});
test('activity distinguishes stale data and guarded actions from inferred stalls',()=>{const b=bot();assert.equal(activityReason(b,now).label,'Possible stall');b.world.inCombat=true;assert.equal(activityReason(b,now).label,'In combat');b.world.at=now-121;assert.equal(activityReason(b,now).label,'Telemetry delayed');b.world.at=now;b.world.inCombat=false;b.world.casting=true;assert.equal(activityReason(b,now).label,'Casting');b.world.casting=false;b.lastDecision.action='rest';assert.equal(activityReason(b,now).label,'Intends to rest');});