Retry native party invitations and remove verified dead memberships
Core tests / core (push) Successful in 1m0s

This commit is contained in:
Codex
2026-09-22 23:18:27 -04:00
parent a533c7b28a
commit ad9f1801e4
6 changed files with 192 additions and 7 deletions
+3 -3
View File
@@ -2,7 +2,7 @@ const {test}=require('node:test');
const assert=require('node:assert/strict');
const {partyDecision,bondEvidence,community,compileCommunityAction,observePresence,applyCooperation}=require('../.test-build/community.js');
const now=()=>Date.now()/1000;
function bot(id){return {id,name:id,status:'online',level:5,autonomy:{managed:true,characterGuid:id==='A'?1:2},personality:{sociability:60,activities:['quest']},relationships:[],memories:[],world:{at:now(),team:1,nearby:id==='A'?['B']:['A'],inCombat:false,groupNames:[],groupId:'',guildId:3,guildName:'Guild',guildRank:0,mapId:0,x:1,y:1}};}
function bot(id){return {id,name:id,status:'online',level:5,autonomy:{managed:true,characterGuid:id==='A'?1:2,accountId:id==='A'?11:12},personality:{sociability:60,activities:['quest']},relationships:[],memories:[],world:{at:now(),team:1,nearby:id==='A'?['B']:['A'],inCombat:false,groupNames:[],groupId:'',guildId:3,guildName:'Guild',guildRank:0,mapId:0,x:1,y:1}};}
test('party acceptance respects faction, level, hostility, combat, and observation freshness',()=>{
const a=bot('A'),b=bot('B');assert.equal(partyDecision(a,b).accepted,true);
b.world.team=0;assert.equal(partyDecision(a,b).accepted,false);b.world.team=1;
@@ -22,7 +22,7 @@ test('relationship evidence is reciprocal, replay safe, bounded, and excludes de
});
test('native commands protect human parties, cap party size, verify identities and enforce guild authority',()=>{
const a=bot('A'),b=bot('B');
const p=compileCommunityAction('party',a,b);assert.match(p,/Human party is protected/);assert.match(p,/GetMembersCount\(\)>=5/);assert.match(p,/GroupCreate/);assert.match(p,/GetGUIDLow\(\)~=2/);
const p=compileCommunityAction('party',a,b);assert.match(p,/AzerothAIQueueParty/);assert.match(p,/guid=2,account=12/);assert.match(p,/__command_id/);
const g=compileCommunityAction('recruit',a,b);assert.match(g,/GetGuildRank\(\)~=0/);assert.match(g,/Already belongs to a guild/);
assert.match(compileCommunityAction('leave',a),/Party changed/);
});
@@ -34,7 +34,7 @@ test('fresh movement snapshots update guild registry and stale snapshots cannot
});
test('offline party members are protected when their identity cannot be checked live',()=>{
assert.match(compileCommunityAction('party',bot('A'),bot('B')),/Party has offline members/);
assert.match(compileCommunityAction('party',bot('A'),bot('B')),/AzerothAIQueueParty/);
assert.match(compileCommunityAction('leave',bot('A')),/Party has offline members/);
});
test('recent danger encourages shy companions without overriding hostility or human control',()=>{
+66
View File
@@ -0,0 +1,66 @@
from pathlib import Path
from lupa import LuaRuntime
setup='''
now=1000;os.time=function()return now end;timers={};receipts={};audit={};people={};rows={{1,'A',11,'active'},{2,'B',12,'active'}};roster={{1,'A',11},{2,'B',12}};removed=0;joins=0
function query(data)local i=1;return {GetUInt32=function(_,n)return data[i][n+1]end,GetString=function(_,n)return data[i][n+1]end,NextRow=function()i=i+1;return i<=#data end}end
function AuthDBQuery()return query(rows)end
function CharDBQuery()return query(roster)end
function AuthDBExecute(s)table.insert(audit,s)end
function GetGUIDLow(g)return g end
function GetPlayersInWorld()return people end
function GetPlayerByName(n)for _,p in pairs(people)do if p.name==n then return p end end end
function CreateLuaEvent(fn,ms)timers[ms]=fn end
function PrintInfo()end;function PrintError(s)error(s)end
function AzerothAIComplete(id,ok,result)receipts[id]={ok,result}end
function AzerothAIEmitSnapshots()end
function make(id,name)
local p={id=id,name=name,dist=20}
function p:GetName()return self.name end;function p:GetGUIDLow()return self.id end;function p:GetAccountId()return 10+self.id end
function p:IsPlayerbot()return not self.human end;function p:IsDead()return self.dead end;function p:IsInCombat()return self.combat end
function p:IsCasting()return false end;function p:IsTaxi()return false end
function p:GetBotAI()return {HasGameClientMaster=function()return self.controlled end}end
function p:GetTeam()return self.team or 0 end;function p:GetLevel()return 5 end;function p:GetMapId()return 0 end;function p:GetPhaseMask()return 1 end
function p:GetDistance()return self.dist end;function p:GetGroup()return self.group end
function p:GroupCreate(b)joins=joins+1;local g=group({self,b},2);self.group=g;b.group=g;return g end
return p
end
function group(members,count)
local g={members=members,count=count}
function g:GetGUID()return 99 end;function g:IsRaidGroup()return false end;function g:GetMembers()return self.members end;function g:GetMembersCount()return self.count end
function g:GetMemberGUID(name)return name=='A' and 1 or 2 end
function g:RemoveMember(id)removed=removed+1;for _,p in pairs(self.members)do p.group=nil end;return true end
function g:AddMember(p)joins=joins+1;p.group=self;table.insert(self.members,p);self.count=self.count+1;return true end
return g
end
A=make(1,'A');B=make(2,'B');people={A,B}
function invite(id,expires)return AzerothAIQueueParty(id or 'x',{name='A',guid=1,account=11},{name='B',guid=2,account=12},expires or now+45)end
function pulse(dt)now=now+dt;timers[2000]()end
function orphan(state)
rows[2][4]=state;people={A};A.group=group({A},2)
timers[10000]()
end
'''
def new():
l=LuaRuntime();l.execute(setup);l.execute((Path(__file__).resolve().parents[1]/'ale/azerothai_parties.lua').read_text());return l
l=new();l.execute("A.combat=true;assert(invite().azerothaiDeferred);pulse(2);assert(not receipts.x and joins==0);A.combat=false;pulse(2);assert(receipts.x[1] and joins==1)")
l=new();l.execute("A.dist=100;invite();pulse(2);assert(not receipts.x);A.dist=40;pulse(2);assert(receipts.x[1])")
l=new();l.execute("A.dist=100;invite();pulse(2);A.dist=20;pulse(44);assert(not receipts.x[1] and joins==0)")
l=new();l.execute("invite();B.controlled=true;pulse(2);assert(not receipts.x[1] and joins==0)")
l=new();l.execute("invite();B.team=1;pulse(2);assert(not receipts.x[1] and joins==0)")
l=new();l.execute("invite();assert(invite().azerothaiDeferred);pulse(2);assert(joins==1)")
l=new();l.execute("orphan('dead');assert(removed==1)")
l=new();l.execute("orphan('active');assert(removed==0)")
l=new();l.execute("rows[2][2]='Unknown';orphan('dead');assert(removed==0)")
l=new();l.execute("A.controlled=true;orphan('dead');assert(removed==0)")
print('Native invitation retries, expiration, idempotence, faction/human guards and verified-death cleanup passed')
# Exercise the actual TypeScript wrapper template, including deferred receipt handling.
import re
source=(Path(__file__).resolve().parents[1]/'core/ale.ts').read_text()
wrapper=re.search(r'return `(local __command_id=.*?)`;',source,re.S).group(1)
l=new();l.execute(wrapper.replace('${qid}',"'wrapped'").replace('${source}',"return AzerothAIQueueParty(__command_id,{name='A',guid=1,account=11},{name='B',guid=2,account=12},1045)"))
l.execute("assert(not receipts.wrapped);pulse(2);assert(receipts.wrapped[1])")
l=new();l.execute(wrapper.replace('${qid}',"'ordinary'").replace('${source}','return 7'));l.execute("assert(receipts.ordinary[1] and receipts.ordinary[2]=='7')")
print('Real bridge wrapper preserves ordinary receipts and waits for deferred native completion')
l=new();l.execute("A.group=group({A},2);invite();pulse(2);assert(not receipts.x and joins==0);pulse(44);assert(not receipts.x[1] and joins==0)")
print('Invitations preserve incomplete living parties while waiting, then expire')