Files
SGF/scripts/workshop-concurrency.php
T
nala 21a91f2aff Accept isolated WORKSHOP-RESOURCE-RECORD-v1 delivery
Record explicit workshop acceptance and preserve subsequent synthetic work without accepting receivables. Keep 080000/070000 unapplied, real workshop grants pending and operational issuance blocks intact. Exclude local credentials and real-data preservation reports.
2026-10-27 11:28:34 +02:00

46 lines
6.6 KiB
PHP

<?php
use App\Modules\Workshop\WorkshopWorkflow;
use App\Shared\Context\CompanyContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\Process\Process;
require __DIR__.'/../vendor/autoload.php';
$app=require __DIR__.'/../bootstrap/app.php';$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();WorkshopWorkflow::guard();
$f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR);$fx=$f['workshop'];$w=app(WorkshopWorkflow::class);
$base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit']];$proof=[];
$select=fn($actor)=>app(CompanyContext::class)->selectFor($actor,$f['tenant'],$f['company'],$f['unit']);
$key=fn()=>(string)Str::uuid();$rev=fn($id)=>(int)DB::selectOne('SELECT sgf_wo_revision(?) AS v',[$id])->v;
$pair=function($one,$two)use($base){
$a=new Process([PHP_BINARY,'scripts/workshop-worker.php','--env=testing'],base_path(),null,json_encode($one+$base+['pause'=>true]));
$b=new Process([PHP_BINARY,'scripts/workshop-worker.php','--env=testing'],base_path(),null,json_encode($two+$base));
$a->start();if(!$a->waitUntil(fn($t,$v)=>str_contains($v,'LOCK_HELD')))throw new RuntimeException('No overlapping lock.');$b->start();$a->wait();$b->wait();$out=[];
foreach([$a,$b] as $p){if(!$p->isSuccessful())throw new RuntimeException('Synthetic worker failed.');$lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($v)=>str_starts_with($v,'{')));$out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR);}return $out;
};
$select($f['manager_id']);
$opening=['method'=>'open','actor'=>$f['manager_id'],'key'=>$key(),'input'=>['reference'=>'OT-REPLAY-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Abertura concorrente.','location'=>'Local de teste','evidence'=>'Pedido sintético.']];
$out=$pair($opening,$opening);if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])throw new RuntimeException('Concurrent opening replay failed.');$proof['same_opening_one_order_one_replay']=true;
$o=$w->open($f['manager_id'],['reference'=>'OT-CONCURRENT-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Prova de concorrência independente.','location'=>'Local de teste','evidence'=>'Somente ensaio isolado.'],$key())['id'];
$w->record($o,$f['manager_id'],['kind'=>'assignment','origin'=>'CONCURRENT-ASSIGN','version'=>1,'revision'=>0,'technician_id'=>$fx['technician_id']],$key());$select($fx['technician_id']);
$d=['kind'=>'task','origin'=>'CONCURRENT-TASK','version'=>1,'revision'=>$rev($o),'description'=>'Tarefa sintética concorrente.'];
$input=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>$d];$out=$pair($input,$input);
if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed']||$out[0]['result']['id']!==$out[1]['result']['id'])throw new RuntimeException('Record replay race failed.');
$task=$out[0]['result']['id'];$proof['same_record_one_insert_one_replay']=true;
$d=['kind'=>'time','task_id'=>$task,'origin'=>'CONCURRENT-TIME-A','version'=>1,'revision'=>$rev($o),'starts_at'=>$fx['day'].'T14:00:00+02:00','ends_at'=>$fx['day'].'T15:00:00+02:00','mode'=>'reported','time_type'=>'work','evidence'=>'Relato retrospectivo sintético.'];
$a=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>$d];$b=$a;$b['key']=$key();$b['input']['origin']='CONCURRENT-TIME-B';$out=$pair($a,$b);
if(array_column($out,'status')!==[200,409])throw new RuntimeException('Overlap race failed.');$time=$out[0]['result']['id'];$proof['concurrent_same_technician_not_double_counted']=true;
$a=['method'=>'verify','id'=>$time,'actor'=>$fx['reviewer_id'],'key'=>$key(),'input'=>['version'=>1,'revision'=>$rev($o),'decision'=>'acknowledged','reason'=>'Somente auditoria; não autoriza execução.']];$out=$pair($a,$a);
if($out[0]['status']!==200||$out[1]['status']!==200||!$out[1]['result']['replayed'])throw new RuntimeException('Verification replay race failed.');$proof['verification_one_decision_one_replay']=true;
// A new version and a decision against its old version cannot both win.
$original=DB::table('wo_entries')->find($time);$a=['method'=>'record','id'=>$o,'actor'=>$fx['technician_id'],'key'=>$key(),'input'=>array_replace($d,['origin'=>$original->origin,'version'=>2,'previous_id'=>$time,'revision'=>$rev($o),'correction_reason'=>'Correcção concorrente explícita.','ends_at'=>$fx['day'].'T14:30:00+02:00'])];
$b=['method'=>'verify','id'=>$time,'actor'=>$fx['reviewer_id'],'key'=>$key(),'input'=>['version'=>1,'revision'=>$rev($o),'decision'=>'refused','reason'=>'Revisão antiga concorrente.']];$out=$pair($a,$b);
if(array_column($out,'status')!==[200,409])throw new RuntimeException('Version versus review race failed.');$proof['new_version_invalidates_old_review']=true;
$row=(array)DB::table('wo_entries')->find($out[0]['result']['id']);unset($row['id']);$row['origin']='SQL-OVERLAP-A';$row['version']=1;$row['previous_id']=null;$row['starts_at']=$fx['day'].'T16:00:00+02:00';$row['ends_at']=$fx['day'].'T17:00:00+02:00';$row['minutes']=60;$row['revision']=$rev($o);
$select($f['manager_id']);
$o2=$w->open($f['manager_id'],['reference'=>'OT-CROSS-'.Str::uuid(),'client_id'=>$fx['client'],'equipment_id'=>DB::table('wo_orders')->find($fx['order'])->equipment_id,'request'=>'Segunda ordem para disputa de tempo.','location'=>'Local de teste','evidence'=>'Somente ensaio de concorrência.'],$key())['id'];
$w->record($o2,$f['manager_id'],['kind'=>'assignment','origin'=>'CROSS-ASSIGN','version'=>1,'revision'=>0,'technician_id'=>$fx['technician_id']],$key());$select($fx['technician_id']);
$t2=$w->record($o2,$fx['technician_id'],['kind'=>'task','origin'=>'CROSS-TASK','version'=>1,'revision'=>1,'description'=>'Tarefa de outra ordem.'],$key())['id'];
$a=['method'=>'sql','actor'=>$fx['technician_id'],'sql'=>$row];$b=$a;$b['sql']['origin']='SQL-OVERLAP-B';$b['sql']['order_id']=$o2;$b['sql']['task_id']=$t2;$b['sql']['revision']=$rev($o2);$out=$pair($a,$b);
if(array_column($out,'status')!==[200,409]||!($out[1]['database_guard']??false))throw new RuntimeException('SQL concurrency guard failed.');$proof['raw_sql_revision_overlap_guard']=true;
file_put_contents(base_path('docs/proofs/workshop-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'synthetic_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo "Six overlapping-process scenarios passed only in sgf_tempo_test; no duplicate order, record, decision or effective time.\n";