feat(v4): consolidate audited data recovery economic replay and optional archival

This commit is contained in:
ENELIX Agent
2026-10-03 10:40:18 +00:00
parent 6c91d6bcc6
commit 8eb9688942
26 changed files with 1747 additions and 38 deletions
@@ -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);}