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')