Files
AzerothAI/tests/realm-insights.test.cjs
T
2026-09-24 19:23:03 +08:00

37 lines
5.5 KiB
JavaScript

const {test}=require('node:test');const assert=require('node:assert/strict');
const {observeInsights,activityReason,reportWindow,summarize}=require('../.test-build/realm-insights.js');
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','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');});
test('pending social events count once after native confirmation',()=>{const s=state();s.community.feed=[{id:'social-1:reply',kind:'conversation',status:'pending',at:new Date(now*1000).toISOString(),names:['A','B']}];const a=observeInsights(s,undefined,now);s.community.feed[0].status='confirmed';const b=observeInsights(s,a.cursor,now+60);assert.equal(b.events.length,1);assert.equal(observeInsights(s,b.cursor,now+120).events.length,0);});
test('enabling failure tracking does not backfill old failed party actions',()=>{const s=state();const a=observeInsights(s,undefined,now);s.community.feed=[{id:'party-old',kind:'party',status:'failed',at:new Date((now-600)*1000).toISOString(),names:['A','B']},{id:'party-new',kind:'party',status:'failed',at:new Date((now+20)*1000).toISOString(),names:['A','B']}];const b=observeInsights(s,a.cursor,now+60);assert.deepEqual(b.events.map(e=>e.id),['social:party-new']);});
test('party time requires fresh consecutive matching living-party observations',()=>{
const s=state();for(const b of s.bots)Object.assign(b.world,{groupId:'g',groupNames:['A','B'],nearby:['A','B']});
let a=observeInsights(s,undefined,now);assert.equal(a.events.length,0);
for(const b of s.bots)b.world.at=now+60;
let c=observeInsights(s,a.cursor,now+60);assert.equal(c.events.filter(e=>e.kind==='nearPartySeconds').reduce((n,e)=>n+e.amount,0),120);
assert.equal(observeInsights(s,c.cursor,now+90).events.length,0,'same snapshot is not more evidence');
for(const b of s.bots)b.world.at=now+500;
assert.equal(observeInsights(s,c.cursor,now+500).events.length,0,'long collection gap excluded');
s.bots[1].world.isDead=true;s.bots[0].world.at=now+90;
assert.equal(observeInsights(s,c.cursor,now+90).events.filter(e=>e.kind==='nearPartySeconds').length,0);
});
test('monitor distinguishes mana recovery, named leader and ghost state',()=>{
const b=bot();Object.assign(b.world,{manaPct:20,survivalAction:'recovering readiness',survivalAt:now,survivalSince:now-70});
assert.equal(activityReason(b,now).label,'Recovering mana');assert.match(activityReason(b,now).detail,/1m 10s/);
b.world.survivalAction='regroup requested';b.world.leaderName='Zaku';assert.equal(activityReason(b,now).label,'Catching up to Zaku');
b.world.isGhost=true;assert.equal(activityReason(b,now).label,'Recovering as a ghost');
});
test('trainer activity reports pending work and verified learning separately',()=>{
const b=bot();Object.assign(b.world,{trainingAt:now,trainingLevel:8,trainingPending:3,trainingLearned:2,trainingTrainer:'Teacher',trainingStatus:'traveling'});
const a=activityReason(b,now);assert.equal(a.label,'Visiting class trainer');assert.match(a.detail,/3 identified spells pending, 2 verified trainer purchases/);
b.world.inCombat=true;assert.equal(activityReason(b,now).label,'In combat');
});