feat(v4): consolidate audited data recovery economic replay and optional archival
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__.'/../../libs/NetzfahrplanV4Datenarchiv.php';
|
||||
use Belevo\EnelixEMS\NetzfahrplanV4Datenarchiv as A;
|
||||
$n=0;$now=1791028800;
|
||||
function checkArchive(bool $x,string $why):void {global $n;if(!$x)throw new RuntimeException('FAIL '.$why);$n++;echo 'PASS '.$why."\n";}
|
||||
function cleanupArchive(string $dir):void {foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST)as$p){$p->isDir()&&!$p->isLink()?rmdir($p->getPathname()):unlink($p->getPathname());}rmdir($dir);}
|
||||
$base=sys_get_temp_dir().'/v4-archive-'.bin2hex(random_bytes(6));mkdir($base,0700);
|
||||
try {
|
||||
$old='raw-'.gmdate('Ymd',$now-10*86400).'.jsonl';$recent='raw-'.gmdate('Ymd',$now-2*86400).'.jsonl';$day='raw-'.gmdate('Ymd',$now).'.jsonl';
|
||||
$raw="{\"sample\":1}\n{\"sample\":2}\n";file_put_contents($base.'/'.$old,$raw);file_put_contents($base.'/'.$recent,$raw);file_put_contents($base.'/'.$day,$raw);
|
||||
checkArchive(A::run($base,[],$now)['archived']===0,'no acknowledgement means no archival');
|
||||
$r=A::run($base,['day'=>$old,'offset'=>5],$now);checkArchive($r['archived']===0&&is_file($base.'/'.$old),'partially acknowledged day retained');
|
||||
$r=A::run($base,['day'=>$day,'offset'=>5],$now);checkArchive($r['archived']===1&&!is_file($base.'/'.$old),'only completely acknowledged old day retired');
|
||||
$files=glob($base.'/archive/*.gz');checkArchive(count($files)===1&&gzdecode(file_get_contents($files[0]))===$raw,'compressed original verified byte for byte');
|
||||
checkArchive(is_file($base.'/'.$recent)&&is_file($base.'/'.$day),'recent and current outbox preserved');
|
||||
checkArchive(A::run($base,['day'=>$day,'offset'=>5],$now)['archived']===0,'repeat is idempotent');
|
||||
file_put_contents($base.'/'.$old,$raw);file_put_contents($files[0],gzencode('corrupt'));
|
||||
try {A::run($base,['day'=>$day,'offset'=>5],$now);throw new LogicException('expected refusal');} catch(RuntimeException $e) {checkArchive(is_file($base.'/'.$old),'archive corruption never loses original');}
|
||||
unlink($files[0]);$lock=fopen($base.'/.writer.lock','c+b');flock($lock,LOCK_EX);
|
||||
checkArchive(A::run($base,['day'=>$day,'offset'=>5],$now)['status']==='writer_busy','archive serializes with actual capture writer');flock($lock,LOCK_UN);fclose($lock);
|
||||
foreach(range(11,15)as$d)file_put_contents($base.'/raw-'.gmdate('Ymd',$now-$d*86400).'.jsonl',$raw);
|
||||
checkArchive(A::run($base,['day'=>$day,'offset'=>5],$now)['archived']===2,'bounded archive work per invocation');
|
||||
$text=file_get_contents(__DIR__.'/../../libs/NetzfahrplanV4Datenarchiv.php');checkArchive(!preg_match('/\b(?:RequestAction|IPS_SetProperty|curl_exec)\s*\(/',$text),'archive has no control or network calls');
|
||||
echo "TOTAL $n archive checks passed; only temporary test data.\n";
|
||||
} finally {cleanupArchive($base);}
|
||||
@@ -63,16 +63,29 @@ try {
|
||||
check($sent[0]['records'][0]['raw']['s101']['sourceUpdatedAt']===$clock,'original timestamp retained');
|
||||
check(!str_contains(json_encode($sent),'synthetic-not-a-real-token'),'token never included in data payload');
|
||||
check($sent[0]['records'][0]['controlEligible']===false,'data delivery is not actuator permission');
|
||||
$expected=hash('sha256',json_encode(NetzfahrplanV4Messaufnahme::configuration($c),JSON_THROW_ON_ERROR|JSON_PRESERVE_ZERO_FRACTION));
|
||||
check($sent[0]['records'][0]['mappingSha256']===$expected,'property roundtrip preserves approved mapping identity');
|
||||
check(json_decode($m->a['NetzfahrplanV4LetzteDatenquittung'],true)['acceptedThrough']===end($sent[0]['records'])['capturedAt'],'successful acknowledgement persisted independently');
|
||||
$clock+=30;$m->ErfasseNetzfahrplanV4Daten();check(count($sent)===1,'30-second collection does not double request rate');
|
||||
check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'unsent rows remain behind cursor');
|
||||
check(isset(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['lastAcknowledgement']['acceptedThrough']),'scheduled status retains last successful receipt');
|
||||
$clock+=30;$mode='429';$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'429 never advances acknowledgement cursor');
|
||||
check(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['delivery']['dataRetained'],'offline data retained');
|
||||
check(json_decode($m->vars['NetzfahrplanV4Datenstatus'],true)['delivery']['httpStatus']===429,'safe HTTP error code visible without credentials');
|
||||
$mode='wrong_ack';$clock+=120;$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']===$cursor,'wrong ack timestamp rejected');
|
||||
$mode='ok';$clock+=240;$m->ErfasseNetzfahrplanV4Daten();check($m->a['NetzfahrplanV4DatenCursor']!==$cursor,'later success sends the preserved backlog');
|
||||
check(count(end($sent)['records'])===4,'all previously unacknowledged rows included once');
|
||||
$file=glob($m->folder().'/raw-*.jsonl')[0];$count=count(file($file));check($count===5,'raw journal never cleared by upload');
|
||||
$lastCursor=$m->a['NetzfahrplanV4DatenCursor'];file_put_contents($file,'incomplete',FILE_APPEND);
|
||||
check($m->send()['status']==='up_to_date','in-progress tail not transmitted');check($m->a['NetzfahrplanV4DatenCursor']===$lastCursor,'partial tail not acknowledged');
|
||||
// Only temporary test journal: turn the prior incomplete tail into an additional complete queued record.
|
||||
$buf=file_get_contents($file);$buf=substr($buf,0,-strlen('incomplete'));
|
||||
$extra=$sent[0]['records'][0];$clock+=60;$extra['capturedAt']=gmdate('c',$clock);$extra['captureStartedAt']=$extra['capturedAt'];
|
||||
file_put_contents($file,$buf.json_encode($extra)."\n".'crash-tail');
|
||||
$clock+=60;$beforeCount=count($sent);$m->ErfasseNetzfahrplanV4Daten();
|
||||
$err=json_decode($m->vars['NetzfahrplanV4Datenstatus'],true);
|
||||
check($err['status']==='capture_error'&&$err['delivery']['status']==='acknowledged','capture failure does not deadlock pending delivery');
|
||||
check(count($sent)===$beforeCount+1&&str_ends_with(file_get_contents($file),'crash-tail'),'complete prefix sent while original incomplete tail retained');
|
||||
$m->p['NetzfahrplanV4MessdatenAktiv']=false;$m->configure();check($m->timers['NetzfahrplanV4DatenErfassen']===0,'disable affects acquisition only');
|
||||
check(count($m->vars)===1,'only own manager data-status variable written');
|
||||
$text=file_get_contents(__DIR__.'/../../libs/ManagerNetzfahrplanV4DatenTrait.php');
|
||||
|
||||
Reference in New Issue
Block a user