Ground bot intentions in live game knowledge and verify outcomes
Core tests / core (push) Successful in 28s

This commit is contained in:
Codex
2026-09-25 13:28:03 +08:00
parent 20ccd0fced
commit 502cb1e4fd
17 changed files with 251 additions and 24 deletions
+26
View File
@@ -35,3 +35,29 @@ test('rest requests never enable indefinite stay',()=>{
const source=compileLivePlayerbotIntent('A',{action:'rest',goal:'Recover',reason:'Tired'});
assert.doesNotMatch(source,/\+stay/);assert.match(source,/-stay/);assert.match(source,/GetHealthPct/);assert.match(source,/AzerothAITrainingOverride/);
});
test('fresh full recovery removes unnecessary rest but stale or low mana does not',()=>{
const {availableIntentActions,decisionContext}=require('../.test-build/autonomy.js');
const bot={personality:{activities:['quest','rest']},world:{at:Date.now()/1000,healthPct:100,manaPct:100,questBrief:'A real quest',threatBrief:'Wolf level 7',readinessAt:Date.now()/1000,foodCount:0,drinkCount:0}};
assert.deepEqual(availableIntentActions(bot),['quest']);assert.match(decisionContext(bot),/A real quest/);assert.match(decisionContext(bot),/Wolf level 7/);
bot.world.manaPct=20;assert.deepEqual(availableIntentActions(bot),['quest','rest']);
bot.world.at-=200;assert.deepEqual(availableIntentActions(bot),['quest','rest']);assert.doesNotMatch(decisionContext(bot),/Wolf/);
});
test('intent outcomes require fresh matching evidence, not just accepted commands',()=>{
const {startIntentCheck,observeIntentChecks}=require('../.test-build/intent-evidence.js');
const now=Date.now()/1000,b={level:10,world:{at:now,x:0,y:0,mapId:0,healthPct:50,manaPct:10,groupNames:[],xp:100},autonomy:{}};
const check=startIntentCheck(b,{action:'quest'},'q',now);b.autonomy.intentChecks=[check];
b.world.x=30;b.world.at=now+30;observeIntentChecks(b);assert.equal(check.status,'pending','Walking is not quest progress');
b.world.xp=110;observeIntentChecks(b);assert.equal(check.status,'observed');
const rest=startIntentCheck(b,{action:'rest'},'r',now+30);b.autonomy.intentChecks=[rest];b.world.healthPct=100;b.world.manaPct=100;
observeIntentChecks(b);assert.equal(rest.status,'pending','Same snapshot cannot prove recovery');
b.world.at=now+60;b.world.inCombat=false;observeIntentChecks(b);assert.equal(rest.status,'observed');
const party=startIntentCheck(b,{action:'socialize'},'p',now+60,'Peer');b.autonomy.intentChecks=[party];b.world.at=now+90;b.world.nearby=['Peer'];observeIntentChecks(b);assert.equal(party.status,'pending');
b.world.groupId='g';b.world.groupNames=['Peer'];observeIntentChecks(b);assert.equal(party.status,'observed');
});
test('death interrupts intentions and unverified work times out honestly',()=>{
const {startIntentCheck,observeIntentChecks}=require('../.test-build/intent-evidence.js');const n=Date.now()/1000;
const b={level:10,world:{at:n,x:0,y:0,mapId:0,groupNames:[],xp:100},autonomy:{}};
const c=startIntentCheck(b,{action:'trade'},'t',n);b.autonomy.intentChecks=[c];b.world.at=n+601;observeIntentChecks(b);assert.equal(c.status,'no_progress');
const d=startIntentCheck(b,{action:'quest'},'q',n+601);b.autonomy.intentChecks=[d];b.world.at=n+630;b.world.isGhost=true;observeIntentChecks(b);assert.equal(d.status,'interrupted');
});
+7
View File
@@ -54,3 +54,10 @@ test('adventuring companions stay within five levels',()=>{
assert.equal(partyDecision(a,b).accepted,false);
b.level=a.level+5;assert.equal(partyDecision(a,b).accepted,true);
});
test('party intentions exclude ghosts and protect unknown party members',()=>{
const {intentCompanions}=require('../.test-build/community.js');const a=bot('A'),b=bot('B'),s={bots:[a,b],community:{partiesEnabled:true}};
assert.deepEqual(intentCompanions(s,a).map(x=>x.name),['B']);
b.world.isGhost=true;assert.equal(intentCompanions(s,a).length,0);b.world.isGhost=false;
a.world.groupId='g';a.world.groupNames=['A','Unknown'];assert.equal(intentCompanions(s,a).length,0);
});
+12
View File
@@ -34,3 +34,15 @@ test('trainer activity reports pending work and verified learning separately',()
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');
});
test('exposure excludes stale snapshots and gaps; outcome evidence counts once',()=>{
const s=state();const first=observeInsights(s,undefined,now);
for(const b of s.bots)b.world.at=now+60;
s.bots[0].autonomy.intentChecks=[{id:'q',action:'quest',status:'observed',checkedAt:now+30,detail:'XP gained'}];
const next=observeInsights(s,first.cursor,now+60);
assert.equal(next.events.filter(e=>e.kind==='onlineSeconds').reduce((n,e)=>n+e.amount,0),120);
assert.equal(next.events.filter(e=>e.kind==='intentObserved').length,1);
assert.equal(observeInsights(s,next.cursor,now+90).events.length,0);
for(const b of s.bots)b.world.at=now+600;
assert.equal(observeInsights(s,next.cursor,now+600).events.filter(e=>e.kind==='onlineSeconds').length,0);
});
+5
View File
@@ -8,3 +8,8 @@ test('resolved old recovery failures do not remain actionable',()=>{const b={id:
test('currency formats copper and remains independent of item counts and historical baselines',()=>{const {formatCopper}=require('../.test-build/realm-outcomes.js');assert.equal(formatCopper(12345),'1g 23s 45c');assert.equal(formatCopper(0),'0g 0s 0c');const first=outcomeChanges([{...row,lootedCopper:100}],{at:1,startedAt:1,rows:[row]},2);assert.equal(first.events.length,0);const next=outcomeChanges([{...row,lootedCopper:250}],first.baseline,3);assert.deepEqual(next.events.map(e=>[e.kind,e.amount]),[['lootedCopper',150]]);});
test('large state chunks preserve Unicode and all history within per-value limits',()=>{const {splitState}=require('../.test-build/state-chunks.js');const input={history:'🎮'.repeat(600000)};const chunks=splitState(input);assert.ok(chunks.length>1);assert.deepEqual(JSON.parse(chunks.join('')),input);for(const c of chunks)assert.ok(Buffer.byteLength(c)<512*1024);});
test('normal death counts establish a baseline and count subsequent native deaths',()=>{
const first=outcomeChanges([{...row,nativeDeaths:100}],{at:1,startedAt:1,rows:[row]},2);assert.equal(first.events.length,0);
const next=outcomeChanges([{...row,nativeDeaths:103}],first.baseline,3);assert.equal(next.events.find(e=>e.kind==='nativeDeaths').amount,3);
});