Add community map records and telemetry API; record Workshop 0.0.126
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""Production map-response validation and delayed delivery, with engine JSON/UI doubled."""
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
base = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
||||
p = base/'ERR_CommunityStatistics.c'
|
||||
valid = body(p, 'bool Valid(')
|
||||
matches = body(p, 'bool Matches(').replace('server.', 'server->').replace('allServers.', 'allServers->')
|
||||
callbacks = '\n'.join(body(base/'ERR_Telemetry.c', name) for name in ['protected void CommunityUnavailable(', 'protected void UpdateCommunity('])
|
||||
for a,b in [
|
||||
('protected ', ''), ('RestCallback callback','RestCallback* callback'), ('callback.', 'callback->'),
|
||||
('JsonLoadContext sent = new JsonLoadContext()', 'Json sent'), ('JsonLoadContext reply = new JsonLoadContext()', 'Json reply'),
|
||||
('ERR_CommunityStatistics community;', 'Community* community = nullptr;'), ('community.', 'community->'),
|
||||
('m_Mode.ERR_PublishCommunityStatistics(', 'mode.Publish('), ('m_State.serverId','serverId'),
|
||||
('GetGame().GetWorldFile()', 'world'),
|
||||
]: callbacks=callbacks.replace(a,b)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
struct string:std::string {using std::string::string;using std::string::operator=;bool IsEmpty()const{return empty();}int Length()const{return size();}};
|
||||
struct Record {int wins=2,losses=1,completed=3,partialMissions=0;
|
||||
'''+valid+r'''
|
||||
};
|
||||
struct Community {int schemaVersion=1;string serverId="server",missionId="mission",eventId="final",terrain="map",outcome="victory";Record local,global;Record* server=&local;Record* allServers=&global;
|
||||
'''+matches+r'''
|
||||
string Summary(){return "map record";}
|
||||
} result;
|
||||
struct Json {
|
||||
string input;
|
||||
bool LoadFromString(string v){input=v;return v=="old"||v=="final"||v=="response";}
|
||||
bool ReadValue(string,string& value){value=input;return true;}
|
||||
bool ReadValue(string,Community*& value){value=&result;return true;}
|
||||
};
|
||||
struct RestCallback {string data="response";string GetData(){return data;}};
|
||||
struct Service {
|
||||
bool m_Ended=true,current=true,m_CommunityReady=false;string m_FinalEventId="final",m_FinalOutcome="victory",m_InFlight="final",serverId="server",m_MissionId="mission",world="map";
|
||||
struct Mode {int writes=0;string text;void Publish(string v){writes++;text=v;}} mode;
|
||||
bool Current(){return current;}
|
||||
'''+callbacks+r'''
|
||||
};
|
||||
int main(){
|
||||
assert(result.Matches("server","mission","final","map","victory"));
|
||||
assert(!result.Matches("other","mission","final","map","victory"));
|
||||
assert(!result.Matches("server","old","final","map","victory"));
|
||||
assert(!result.Matches("server","mission","old-event","map","victory"));
|
||||
assert(!result.Matches("server","mission","final","another-map","victory"));
|
||||
assert(!result.Matches("server","mission","final","map","defeat"));
|
||||
result.schemaVersion=2;assert(!result.Matches("server","mission","final","map","victory"));result.schemaVersion=1;
|
||||
result.global.wins=1;result.global.completed=2;assert(!result.Matches("server","mission","final","map","victory"));result.global=Record{};
|
||||
result.local.wins=-1;assert(!result.local.Valid());result.local=Record{};
|
||||
result.local.completed=4;assert(!result.local.Valid());result.local=Record{};
|
||||
result.local.partialMissions=4;assert(!result.local.Valid());result.local=Record{};
|
||||
Service service;RestCallback cb;
|
||||
service.m_InFlight="old";service.UpdateCommunity(&cb);assert(service.mode.writes==0);
|
||||
service.m_InFlight="final";service.current=false;service.UpdateCommunity(&cb);assert(service.mode.writes==0);
|
||||
service.current=true;service.m_Ended=false;service.UpdateCommunity(&cb);assert(service.mode.writes==0);
|
||||
service.m_Ended=true;service.UpdateCommunity(&cb);assert(service.mode.text=="map record");
|
||||
int receivedWrites=service.mode.writes;service.CommunityUnavailable();assert(service.mode.writes==receivedWrites); // an older queued upload failing cannot erase confirmed totals
|
||||
service.m_CommunityReady=false;
|
||||
result.terrain="other-map";service.UpdateCommunity(&cb);assert(service.mode.text.find("unavailable")!=std::string::npos);result.terrain="map";
|
||||
service.UpdateCommunity(&cb);assert(service.mode.text=="map record"); // later valid retry refreshes the fallback
|
||||
service.m_CommunityReady=false;
|
||||
cb.data="invalid-json";service.UpdateCommunity(&cb);assert(service.mode.text.find("unavailable")!=std::string::npos);
|
||||
cb.data=string(8193,'x');service.UpdateCommunity(&cb);assert(service.mode.text.find("unavailable")!=std::string::npos);
|
||||
cb.data="response";service.m_FinalOutcome="interrupted";int before=service.mode.writes;service.UpdateCommunity(&cb);assert(service.mode.writes==before);
|
||||
service.m_FinalOutcome="defeat";result.outcome="defeat";service.UpdateCommunity(&cb);assert(service.mode.text=="map record");
|
||||
service.m_CommunityReady=false;
|
||||
result.local.losses=0;result.local.completed=2;service.UpdateCommunity(&cb);assert(service.mode.text.find("unavailable")!=std::string::npos);
|
||||
}
|
||||
''')
|
||||
mode=(base/'ERR_RoundStatisticsLifecycle.c').read_text()
|
||||
assert '[RplProp(onRplName: "ERR_OnRoundStatisticsReplicated")]\n protected string m_ERRCommunitySummary' in mode
|
||||
publish=body(base/'ERR_RoundStatisticsLifecycle.c','void ERR_PublishCommunityStatistics(')
|
||||
assert '!Replication.IsServer()' in publish and 'Replication.BumpMe(); ERR_OnRoundStatisticsReplicated();' in publish
|
||||
assert 'm_ERRCommunitySummary + "\\n\\n" + m_ERRRoundSummary' in mode
|
||||
sender=(base/'ERR_Telemetry.c').read_text()
|
||||
assert 'm_State.pending.InsertAt(payload, 0);' in sender
|
||||
assert 'GetGame().GetCallqueue().CallLater(Pulse, 1, false)' in sender
|
||||
print('PASS: map/server/mission/event/outcome validation, bounded counters, stale delivery exclusion, current-round refresh and unavailable recovery')
|
||||
@@ -7,6 +7,7 @@ def cpp(v):
|
||||
return v
|
||||
rules='\n'.join(cpp(body(p,x)) for x in ['static bool Acknowledged(', 'static int RetrySeconds(', 'static string Outcome('])
|
||||
methods='\n'.join(cpp(body(p,x)) for x in ['bool Enabled(', 'void Refresh(', 'protected void Purge(', 'void Finish(', 'void PhaseChanged(', 'protected void Delivered(', 'protected void Failed(', 'void Close('])
|
||||
methods=methods.replace('GetGame().GetCallqueue().CallLater(Pulse, 1, false)', 'pulses++')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -34,6 +35,7 @@ struct Service {
|
||||
State state;State* m_State=&state;RestCallback* m_Callback=nullptr;void* m_Context=nullptr;void* m_Mode=nullptr;
|
||||
string m_InFlight;int m_Failures=0,m_Delivered=0,m_NextAttempt=0,samples=0,events=0,removed=0,saves=0;
|
||||
string type,result;
|
||||
int pulses=0;void UpdateCommunity(RestCallback*){}void CommunityUnavailable(){}
|
||||
bool Current(){return current;}
|
||||
void Sample(){samples++;}
|
||||
void Event(string t,string o="",string p="",string prior=""){type=t;result=o;events++;}
|
||||
@@ -86,7 +88,7 @@ assert lifecycle.index('m_ERRTelemetry.Finish(ERR_TelemetryRules.Outcome') < lif
|
||||
assert '#ifdef WORKBENCH' in lifecycle and 'errTelemetryTest' in lifecycle
|
||||
assert 'GetRestApi' not in lifecycle
|
||||
assert 'm_State.pending.Count() >= 64' in s and 'payload.Length() > 65536' in s
|
||||
assert s.index('if (m_Dirty && !Save()) return;') < s.index('m_Context.POST(')
|
||||
assert s.index('if (m_Dirty && !Save())') < s.index('m_Context.POST(')
|
||||
assert 'ReadValue("token"' in s and 'WriteValue("token", m_Config.Token)' in s
|
||||
# Persisted event data must not contain authentication material.
|
||||
event=body(p,'protected void Event(')
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Execute production credential persistence with storage/JSON boundary doubles."""
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryCredentials.c'
|
||||
methods = '\n'.join(body(p, s) for s in ['static bool Valid(', 'static bool Resolve('])
|
||||
for a, b in [
|
||||
('out string token', 'string& token'),
|
||||
('ref array<ref ERR_TelemetryCredential> entries = {};', 'Entries entries;'),
|
||||
('ERR_TelemetryCredential entry', 'Credential* entry'),
|
||||
('ERR_TelemetryCredential known', 'Credential* known'),
|
||||
('ERR_TelemetryCredential created = new ERR_TelemetryCredential()', 'Credential* created = new Credential()'),
|
||||
('JsonLoadContext loaded = new JsonLoadContext()', 'JsonLoadContext loaded'),
|
||||
('JsonSaveContext saved = new JsonSaveContext()', 'JsonSaveContext saved'),
|
||||
('foreach (Credential* known :', 'for (Credential* known :'),
|
||||
('entry.', 'entry->'), ('known.', 'known->'), ('created.', 'created->'),
|
||||
('FileIO.', 'FileIO::'), ('UUID.', 'UUID::'), ('ERR_ProfileConfig.', 'Profile::'),
|
||||
]:
|
||||
methods = methods.replace(a, b)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
struct string : std::string {
|
||||
using std::string::string; using std::string::operator=;
|
||||
string(std::string s):std::string(s){} bool IsEmpty() const{return empty();} int Length()const{return size();}
|
||||
};
|
||||
struct Credential {string endpoint,serverId,token;};
|
||||
struct Entries:std::vector<Credential*> {
|
||||
int Count(){return size();} void Insert(Credential* v){push_back(v);} bool operator!(){return false;}
|
||||
};
|
||||
std::map<std::string,string> files;
|
||||
std::map<std::string,Entries> documents;
|
||||
bool failSave=false,failCopy=false,failRead=false;int generated=0,serial=0;
|
||||
namespace UUID {bool IsUUID(string s){return s=="server"||s=="other";}
|
||||
string GenV4(){generated++;return string(36, char('a'+generated%20));}}
|
||||
namespace FileIO {
|
||||
bool FileExists(string p){return files.count(p);}
|
||||
bool CopyFile(string a,string b){if(failCopy)return false;files[b]=files[a];return true;}
|
||||
void DeleteFile(string p){files.erase(p);}
|
||||
}
|
||||
namespace Profile {bool Text(string p,string& text){if(failRead||!files.count(p))return false;text=files[p];return true;}}
|
||||
struct JsonLoadContext {
|
||||
string raw;
|
||||
bool LoadFromFile(string p){raw=files[p];return documents.count(raw);}
|
||||
bool ReadValue(string,Entries& e){e=documents[raw];return true;}
|
||||
};
|
||||
struct JsonSaveContext {
|
||||
string raw;
|
||||
void EnableTypeDiscriminator(bool){}
|
||||
void WriteValue(string,Entries e){raw="document-"+std::to_string(++serial);documents[raw]=e;}
|
||||
bool SaveToFile(string p){if(failSave)return false;files[p]=raw;return true;}
|
||||
string SaveToString(){return raw;}
|
||||
};
|
||||
struct Credentials {
|
||||
static inline string PATH="credentials.json";
|
||||
'''+methods+r'''
|
||||
};
|
||||
int main(){
|
||||
string token;
|
||||
assert(Credentials::Resolve("https://one", "server", token));
|
||||
assert(token.Length()==72&&generated==2&&files.count(Credentials::PATH));
|
||||
string first=token;assert(!files.count(Credentials::PATH+".tmp"));
|
||||
token="";assert(Credentials::Resolve("https://one", "server", token));
|
||||
assert(token==first&&generated==2); // mission restart reuses the durable secret
|
||||
assert(Credentials::Resolve("https://two", "server", token)&&token!=first);
|
||||
assert(Credentials::Resolve("https://one", "server", token)&&token==first); // endpoint isolation
|
||||
assert(Credentials::Resolve("https://one", "other", token)&&token!=first); // identity isolation
|
||||
failSave=true;assert(!Credentials::Resolve("https://save-fail", "server", token)&&token.IsEmpty());failSave=false;
|
||||
failCopy=true;assert(!Credentials::Resolve("https://copy-fail", "server", token)&&token.IsEmpty());failCopy=false;
|
||||
failRead=true;assert(!Credentials::Resolve("https://verify-fail", "server", token)&&token.IsEmpty());failRead=false;
|
||||
assert(Credentials::Resolve("https://one", "server", token)&&token==first);
|
||||
auto before=generated;files[Credentials::PATH]="corrupt";
|
||||
assert(!Credentials::Resolve("https://one", "server", token)&&token.IsEmpty()&&generated==before);
|
||||
assert(files[Credentials::PATH]=="corrupt"); // preserve damaged evidence; do not silently rotate
|
||||
files.clear();documents.clear();
|
||||
for(int i=0;i<16;i++)assert(Credentials::Resolve("endpoint-"+std::to_string(i),"server",token));
|
||||
assert(!Credentials::Resolve("endpoint-overflow","server",token)&&token.IsEmpty());
|
||||
}
|
||||
''')
|
||||
sender = p.with_name('ERR_Telemetry.c')
|
||||
pulse = sender.read_text().split('protected void Pulse(', 1)[1].split('protected void Delivered(', 1)[0]
|
||||
assert pulse.index('if (!Enabled()) return;') < pulse.index('ERR_TelemetryCredentials.Resolve')
|
||||
assert pulse.index('if (m_Dirty && !Save())') < pulse.index('ERR_TelemetryCredentials.Resolve') < pulse.index('m_Context.POST(')
|
||||
assert 'if (m_Config.Token.IsEmpty())' in pulse
|
||||
assert 'Token' not in body(sender, 'protected void Event(')
|
||||
assert 'Credentials' not in body(sender, 'protected void Purge(') # opt-out keeps identity, deletes queued personal data
|
||||
assert 'https://co10escape-telemetry.zacekjakub.workers.dev/api/mission-events' in (ROOT/'addons/core/Configs/ERR/Telemetry.conf').read_text()
|
||||
print('PASS: durable installation credential, restart/endpoint/identity isolation, write failure, corruption, bounded retention and disabled-send gates')
|
||||
Reference in New Issue
Block a user