table($t)->orderBy('id')->cursor()as$r)hash_update($h,json_encode($r,JSON_THROW_ON_ERROR));}return hash_final($h);};$before=$fingerprint();$grants=DB::table('permission_grants')->orderBy('id')->get()->toJson(); $socket=stream_socket_server('tcp://127.0.0.1:0');$port=(int)substr(strrchr(stream_socket_get_name($socket,false),':'),1);fclose($socket);$server=new Symfony\Component\Process\Process([PHP_BINARY,'-S','127.0.0.1:'.$port,__DIR__.'/stock-ledger-observer-router.php'],dirname(__DIR__),['STOCK_OBSERVER_SETTINGS'=>$s['root'].'/settings.json'],null,60);$server->start(); try{for($i=0;$i<80;$i++){if($sock=@fsockopen('127.0.0.1',$port,$e,$m,.1)){fclose($sock);break;}usleep(50000);} $client=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>15]);$sessions=[];foreach(['reader','limited','foreign-unit','foreign-company']as$role){$jar=new GuzzleHttp\Cookie\CookieJar();$r=$client->get('/qa/stock-observer/'.$role,['cookies'=>$jar]);$ok($r->getStatusCode()===200,'SGF QA session '.$role);$sessions[$role]=['jar'=>$jar]+json_decode((string)$r->getBody(),true,32,JSON_THROW_ON_ERROR);} $call=function($role,$url,$extra=[])use($client,&$sessions){$v=$sessions[$role];$r=$client->get($url,['cookies'=>$v['jar'],'headers'=>array_replace(['Accept'=>'application/json','X-SGF-Context'=>$v['context']],$extra)]);$text=(string)$r->getBody();return ['status'=>$r->getStatusCode(),'data'=>json_decode($text,true),'body'=>$text,'cache'=>$r->getHeaderLine('Cache-Control')];}; $r=$call('reader','/api/stock-valuation-read?per_page=100');$ok($r['status']===200,'SGF reads canonical source with explicit dual rights');$d=$r['data'];$ok($d['physical_quantity_milli']===1500&&$d['posted_stock_value_minor']===7500&&$d['pagination']['total']===3&&$d['status']==='reconciled','SGF sees actual1.5UN and posted75 from same native book');$ok($d['operation_executed']===false&&$d['source_identity']['role']===$s['source']['username']&&count($d['lines'])===6,'read-only canonical source identity and six lines');$ok(str_contains($r['cache'],'no-store'),'SGF response no-store'); $ok($call('reader','/api/stock-valuation-read?page=2')['data']['journals']===[],'SGF server pagination boundary');$ok($call('reader','/api/stock-valuation-read?per_page=26')['status']===422,'SGF invalid page size refused');$ok($call('reader','/api/stock-valuation-read?tenant_id=999')['status']===422,'SGF protected scope parameter refused');$ok($call('reader','/api/stock-valuation-read', ['X-SGF-Context'=>'stale'])['status']===409,'SGF stale tab refused'); $ok($call('limited','/api/stock-valuation-read')['status']===403,'general ledger view alone cannot read stock evidence');$ok($call('foreign-unit','/api/stock-valuation-read')['status']===403,'foreign SGF unit refused');$ok($call('foreign-company','/api/stock-valuation-read')['status']===403,'foreign SGF company refused'); $ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'missing'])['status']===503,'missing binding is unavailable not zero');$ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'wrong-scope'])['status']===503,'native scope mismatch refused');$ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'wrong-role'])['status']===403,'unapproved native role refused'); $r=$call('reader','/general-ledger/stock-valuation');$ok($r['status']===200&&str_contains($r['body'],'75,00')&&str_contains($r['body'],'1500')&&!str_contains($r['body'],'