Files

128 lines
11 KiB
Python

"""Production service/state transitions with native HTTP and profile IO substituted."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpeechService.c'
w=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_WelcomeVoice.c'
def adapt(s):
s=s.replace('protected ', '').replace('ref ', '').replace('null','nullptr')
for cls in ['ERR_AdminSession','ERR_SpeechRequest','ERR_SpeechReceiver','ERR_GameMode','JsonSaveContext','JsonLoadContext']:
for name in ['editor','request','expired','receiver','mode','json']:
s=s.replace(cls+' '+name,cls+'* '+name)
s=s.replace('array<string> values','Array* values')
for name in ['json','m_Mode','m_Request','request','expired','editor','receiver','values']:
s=s.replace(name+'.',name+'->')
for name in ['Editor','Receiver']:
s=s.replace(name+'.',name+'->')
for cls in ['ERR_WelcomeVoiceRules','ERR_SpeechRules','ERR_ProfileConfig','Replication','ERR_GameMode']:
s=s.replace(cls+'.',cls+'::')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
return s
rules='struct ERR_SpeechRules {\n'+'\n'.join(adapt(body(p,s)) for s in ['static bool Endpoint(', 'static string Normalize(', 'static bool Token(', 'static bool Text('])+'\n};'
methods=['bool Bundled()', 'string StaticURL()', 'bool Ready()', 'bool Busy()', 'bool Current()', 'bool IdleFor(',
'void ERR_SpeechService(', 'void Snapshot(', 'protected bool Save(',
'void SetRecordings(', 'void Disable(', 'void TestAndEnable(', 'protected void Begin(',
'bool Request(', 'void Tick()', 'void Finished(', 'void Stop()']
code='\n'.join(adapt(body(p,s)) for s in methods).replace('void ERR_SpeechService(', 'void Init(')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
struct string:std::string { using std::string::string; string(std::string s):std::string(s){}
int Length(){return size();} bool IsEmpty(){return empty();} int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
bool EndsWith(string s){return size()>=s.size()&&compare(size()-s.size(),s.size(),s)==0;}
void TrimInPlace(){auto a=find_first_not_of(" \r\n\t");if(a==npos){clear();return;}auto b=find_last_not_of(" \r\n\t");*this=substr(a,b-a+1);}
};
struct Array:std::vector<string>{void Insert(string s){push_back(s);}};
bool http=true,server=true,loadOK=true,saveOK=true;int saves=0,starts=0;
std::map<string,string> profile,saved;
struct ERR_WelcomeVoiceRules { static bool HTTPReady(){return http;}
'''+body(w,'static bool URL(')+r'''
};
'''+rules+r'''
struct JsonLoadContext { bool ReadValue(string k,string& v){if(!profile.count(k))return false;v=profile[k];return true;} bool ReadValue(string k,bool& v){if(!profile.count(k))return false;v=profile[k]=="true";return true;} };
struct JsonSaveContext {std::map<string,string> data;void EnableTypeDiscriminator(bool){} void WriteValue(string k,string v){data[k]=v;}void WriteValue(string k,bool v){data[k]=v?"true":"false";} };
namespace ERR_ProfileConfig {string VOICE="voice";bool Load(string,JsonLoadContext*){return loadOK;}bool Save(string,JsonSaveContext* json){if(!saveOK)return false;saves++;saved=json->data;return true;} }
namespace Replication {bool IsServer(){return server;}}
struct ERR_GameMode {static ERR_GameMode* live;static ERR_GameMode* Get(){return live;}bool active=true;int restored=0,published=0;bool pending=false;string lastURL,lastText;
bool recordings=true;bool ERR_RecordingsEnabled(){return recordings;}void ERR_SetRecordingsEnabled(bool v){recordings=v;}
bool bundled=false;bool ERR_UsesBundledWelcome(){return bundled;}
bool Active(){return active;}string ERR_CurrentWelcomeText(){return "Welcome to Nogova.";}
void ERR_RestoreStaticWelcome(){restored++;pending=false;}
void ERR_PublishWelcome(string url,string text){published++;lastURL=url;lastText=text;pending=false;}
void ERR_SetWelcomePending(bool value){pending=value;}
};ERR_GameMode* ERR_GameMode::live=nullptr;
struct Game {float now=100000;Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
Game* GetGame(){return &game;}
struct ERR_AdminSession {bool valid=true;Array replies;int previews=0;bool Valid(){return valid;}void SpeechReply(string s){replies.Insert(s);}void SpeechPreview(string,string){previews++;}};
struct ERR_SpeechReceiver {int completed=0,failed=0;void Complete(string,string){completed++;}void Failed(){failed++;}};
struct ERR_SpeechService;
struct ERR_SpeechRequest {string Text,Endpoint,Token;bool Enable=false,Boot=false,cancelled=false;ERR_AdminSession* Editor=nullptr;ERR_SpeechReceiver* Receiver=nullptr;
void Start(ERR_SpeechService*){starts++;}void Cancel(){cancelled=true;Token="";Editor=nullptr;Receiver=nullptr;}
};
struct ERR_SpeechService {
ERR_GameMode* m_Mode=nullptr;string m_Endpoint,m_Token,m_StaticURL,m_Status="Not configured";
bool m_Bundled=true;bool m_Enabled=false,m_Ready=false,m_BootAttempted=false,m_Stopped=false;
ERR_SpeechRequest* m_Request=nullptr;float m_Deadline=0,m_LastTest=-60000,m_LastRequest=-5000;
'''+code+r'''
};
int main(){
assert(ERR_SpeechRules::Endpoint("https://speech.example/"));
for(string url:{"", "https://user:key@host/", "https://host/?key=secret", "https://host/#fragment", "https://host/|oops", "http://bad\tpath"}) assert(!ERR_SpeechRules::Endpoint(url));
assert(ERR_SpeechRules::Normalize(" http://lan:18947 ")=="http://lan:18947/");
assert(ERR_SpeechRules::Token(string(32,'A')));assert(!ERR_SpeechRules::Token(string(31,'A')));assert(!ERR_SpeechRules::Token(string(257,'A')));
assert(!ERR_SpeechRules::Text("\n"));assert(!ERR_SpeechRules::Text(string(801,'A')));
ERR_GameMode mode;ERR_GameMode::live=&mode;ERR_AdminSession admin,other;
profile={{"welcomeUrl","http://old/welcome.b64"}};
ERR_SpeechService service;service.Init(&mode);assert(service.StaticURL()=="http://old/welcome.b64");service.Tick();assert(starts==0&&saves==0&&!service.Ready());
mode.bundled=true;int beforeStarts=starts;service.Tick();assert(service.Bundled()&&starts==beforeStarts&&!service.Busy());mode.bundled=false;
ERR_SpeechReceiver receiver;assert(!service.Request("Hello",&receiver));
string secret="AbCdefghijklmnopqrstuvwxyz0123456789";
admin.valid=false;service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==0);admin.valid=true;
service.TestAndEnable(&admin,"", "");assert(starts==0&&saves==0);
http=false;service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==0);http=true;
service.TestAndEnable(&admin,"https://speech",secret);auto first=service.m_Request;assert(first&&starts==1&&first->Token==secret&&!service.Ready()&&saves==0);
service.TestAndEnable(&other,"https://other/",secret);assert(starts==1);
service.Finished(first,"");assert(saves==0&&!service.Ready()&&service.StaticURL()=="http://old/welcome.b64");
service.TestAndEnable(&admin,"https://speech/",secret);assert(starts==1);game.now+=60001;
service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;saveOK=false;service.Finished(first,"https://speech/clip");assert(!service.Ready()&&saves==0);saveOK=true;
game.now+=60001;service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;
admin.valid=false;service.Finished(first,"https://speech/clip");assert(saves==0&&!service.Ready());admin.valid=true;
game.now+=60001;service.TestAndEnable(&admin,"https://speech/",secret);first=service.m_Request;
service.Finished(first,"https://speech/clip");assert(saves==1&&service.Ready()&&admin.previews==1&&mode.published==1);
assert(!service.Bundled()&&saved["bundledSpeech"]=="false");
assert(saved["briefingToken"]==secret&&saved["welcomeUrl"]=="http://old/welcome.b64"&&saved["ttsEnabled"]=="true");
Array snapshot;service.Snapshot(&snapshot);for(string line:snapshot)assert(line.find(secret)==string::npos);
game.now+=60001;service.TestAndEnable(&admin,"https://other/", "");assert(!service.Busy()&&saves==1);
service.TestAndEnable(&admin,"https://speech/", "");assert(service.m_Request->Token==secret);first=service.m_Request;
service.Finished(first,"");assert(service.Ready()&&saves==1); // failed candidate preserves working config
game.now+=60001;service.TestAndEnable(&admin,"https://speech/", "");first=service.m_Request;
service.Disable(&other);assert(!service.Ready()&&!service.Busy()&&first->cancelled&&saved["ttsEnabled"]=="false");
service.Finished(first,"https://speech/late");assert(!service.Ready()&&admin.previews==1); // late success cannot re-enable
assert(service.Bundled()&&saved["bundledSpeech"]=="true");
profile=saved;ERR_SpeechService disabled;disabled.Init(&mode);disabled.Tick();assert(!disabled.Ready()&&!disabled.Busy());
profile["ttsEnabled"]="true";profile["bundledSpeech"]="false";ERR_SpeechService boot;boot.Init(&mode);int previous=starts;http=false;boot.Tick();assert(starts==previous);http=true;
boot.Tick();assert(boot.Busy()&&mode.pending);first=boot.m_Request;assert(first->Boot);
boot.Finished(first,"https://speech/cached");assert(boot.Ready()&&!mode.pending&&saves==2);
assert(!boot.IdleFor(65000));game.now+=65000;assert(boot.IdleFor(65000));
game.now+=6000;assert(boot.Request("Enemy patrol nearby.",&receiver));first=boot.m_Request;assert(!boot.Request("more",&receiver));
boot.Finished(first,"https://speech/line");assert(receiver.completed==1&&boot.Ready());
game.now+=6000;assert(boot.Request("second",&receiver));first=boot.m_Request;
game.now+=100001;boot.Tick();assert(boot.Ready()&&!boot.Busy()&&receiver.failed==1&&first->cancelled);
boot.Finished(first,"https://speech/late");assert(receiver.completed==1&&boot.Ready());
ERR_SpeechService badBoot;badBoot.Init(&mode);badBoot.Tick();assert(badBoot.Busy());
badBoot.Finished(badBoot.m_Request,"");assert(!badBoot.Ready()&&!badBoot.Busy());
// A recordings toggle preserves live preferences and credentials, and commits only on successful save.
assert(mode.recordings);int beforeSaves=saves;
admin.valid=false;boot.SetRecordings(&admin,false);assert(mode.recordings&&saves==beforeSaves);admin.valid=true;
saveOK=false;boot.SetRecordings(&admin,false);assert(mode.recordings&&saves==beforeSaves);saveOK=true;
boot.SetRecordings(&admin,false);assert(!mode.recordings&&saved["recordingsEnabled"]=="false"&&saved["bundledSpeech"]=="false"&&saved["ttsEnabled"]=="true"&&saved["briefingToken"]==secret);
Array muted;boot.Snapshot(&muted);bool found=false;for(string line:muted)if(line=="recordingsEnabled=0")found=true;assert(found);
profile=saved;ERR_SpeechService reload;reload.Init(&mode);assert(!mode.recordings);
reload.Disable(&admin);assert(!mode.recordings&&saved["recordingsEnabled"]=="false");
reload.SetRecordings(&admin,true);assert(mode.recordings&&saved["recordingsEnabled"]=="true"&&saved["bundledSpeech"]=="true");
profile.erase("recordingsEnabled");mode.recordings=false;ERR_SpeechService legacy;legacy.Init(&mode);assert(mode.recordings);
boot.Stop();assert(!boot.Current()&&!boot.IdleFor(0));
}
''')
print('PASS: production TTS settings/test/enable/failure/disable/timeout flows preserve fallback, secrets, authorization, saved state and late-callback cancellation')