originalRightsConstraint !== null) DB::transaction(function () { DB::table('permission_grants')->whereIn('user_id',$this->qaActors)->where('permission','stock.view')->delete(); DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check'); DB::statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check '.$this->originalRightsConstraint); }); } finally { parent::tearDown(); } } private function fixture(int $n): void { $run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN'); $this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D',$run); $db = DB::selectOne("SELECT current_database() AS name,current_user AS role,inet_server_port() AS port,current_setting('cluster_name') AS cluster"); $this->assertSame('sgf_tempo_test',$db->name); $this->assertSame('sgf_test_runner',$db->role); $this->assertSame(55439,(int)$db->port); $this->assertSame('sgf_modular_regression_'.$run,$db->cluster); if ($this->originalRightsConstraint === null) { $this->originalRightsConstraint = DB::selectOne("SELECT pg_get_constraintdef(oid) AS definition FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->definition; DB::transaction(function () { $old=substr($this->originalRightsConstraint,6); DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check'); DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (($old) OR (permission='stock.view' AND unit_id IS NOT NULL))"); }); } $this->schema = 'sgf_stock_read_'.bin2hex(random_bytes(6)); $s=$this->schema; DB::transaction(function () use ($n,$s) { $this->f=IdentityFixture::create('-stock-'.bin2hex(random_bytes(5))); $this->qaActors[]=$this->f['manager']->id; IdentityFixture::grant($this->f['manager']->id,$this->f['tenant'],$this->f['company'],'stock.view',$this->f['unit']); DB::statement("CREATE SCHEMA $s"); DB::statement("CREATE TABLE $s.stock_items(id bigint PRIMARY KEY,tenant_id bigint NOT NULL,code text NOT NULL,name text NOT NULL,unit text NOT NULL)"); DB::statement("CREATE TABLE $s.warehouses(id bigint PRIMARY KEY,tenant_id bigint NOT NULL,code text NOT NULL,name text NOT NULL)"); DB::statement("CREATE TABLE $s.stock_movements(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,stock_item_id bigint NOT NULL,warehouse_id bigint NOT NULL,quantity_milli bigint NOT NULL,movement_type text NOT NULL,snapshot jsonb NOT NULL,moved_at timestamp NOT NULL)"); DB::statement("INSERT INTO $s.warehouses VALUES(11,77,'WH-A','Synthetic A'),(12,77,'WH-B','Synthetic B'),(13,77,'HIDDEN','Restricted warehouse'),(21,88,'FOREIGN','Foreign warehouse')"); DB::statement("INSERT INTO $s.stock_items SELECT i,77,'QA-'||lpad(i::text,6,'0'),'Synthetic item '||i,'un' FROM generate_series(1,$n) i"); DB::statement("INSERT INTO $s.stock_items VALUES($n+1,77,'ZERO','Without movements','un'),(9999999,88,'SECRET','Foreign item','un')"); DB::statement("INSERT INTO $s.stock_movements(tenant_id,stock_item_id,warehouse_id,quantity_milli,movement_type,snapshot,moved_at) SELECT 77,i,11,10000,'receipt',jsonb_build_object('reference','QA-R-'||i,'private_document','DO_NOT_RETURN'),'2026-11-01'::timestamp FROM generate_series(1,$n) i"); DB::statement("INSERT INTO $s.stock_movements(tenant_id,stock_item_id,warehouse_id,quantity_milli,movement_type,snapshot,moved_at) SELECT 77,i,11,-2000,'issue',jsonb_build_object('reference','QA-I-'||i),'2026-11-02'::timestamp FROM generate_series(1,$n) i"); DB::statement("INSERT INTO $s.stock_movements(tenant_id,stock_item_id,warehouse_id,quantity_milli,movement_type,snapshot,moved_at) VALUES (77,1,13,99000,'hidden','{}',now()),(88,9999999,21,88000,'foreign','{}',now())"); DB::statement("CREATE FUNCTION $s.immutable() RETURNS trigger LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''Inventory ledger is append-only''; END'"); DB::statement("CREATE TRIGGER immutable BEFORE UPDATE OR DELETE ON $s.stock_movements FOR EACH ROW EXECUTE FUNCTION $s.immutable()"); }); $this->key=$this->f['tenant'].':'.$this->f['company'].':'.$this->f['unit']; config(['stock_read.synthetic_sources.'.$this->key=>['schema'=>$s,'source_tenant_id'=>77,'warehouse_ids'=>[11,12],'append_only'=>true], 'modules.companies.'.$this->f['tenant'].':'.$this->f['company']=>['contracted'=>['stock'],'enabled'=>['stock']]]); $this->actingAs($this->f['manager'])->withSession(['auth_version'=>$this->f['manager']->auth_version]); $token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>$this->f['unit'],'module_id'=>'stock'])->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token); } public function test_scoped_exact_reading_and_common_projection_equivalence(): void { $this->fixture(30); foreach (['items'=>31,'warehouses'=>2,'balances'=>30,'movements'=>60] as $view=>$total) { $r=$this->getJson('/api/stock-read?view='.$view)->assertOk()->assertJsonPath('pagination.total',$total)->assertJsonPath('capabilities.write',false); $this->assertStringNotContainsString('DO_NOT_RETURN',$r->getContent()); $this->assertStringNotContainsString('Foreign',$r->getContent()); $this->assertStringNotContainsString('Restricted warehouse',$r->getContent()); $direct=DB::transaction(function () use ($view) { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY'); return (new \Tempo\Inventory\InventoryReader(DB::connection(),$this->schema,77,[11,12]))->page(['view'=>$view,'page'=>1,'per_page'=>25]); }); $this->assertSame(json_decode(json_encode($direct['rows']),true),$r->json('rows')); } $this->getJson('/api/stock-read')->assertOk()->assertJsonPath('rows.0.physical_milli','8000')->assertJsonPath('rows.0.available_milli',null); $this->getJson('/api/stock-read?view=items&search=ZERO')->assertOk()->assertJsonPath('pagination.total',1); $this->getJson('/api/stock-read?warehouse_id=12')->assertOk()->assertJsonPath('pagination.total',0); $this->getJson('/api/stock-read?view=items&search=%25')->assertOk()->assertJsonPath('pagination.total',0); $this->getJson('/api/stock-read?warehouse_id=13')->assertForbidden(); $this->getJson('/api/stock-read?warehouse_id=21')->assertForbidden(); $this->getJson('/api/stock-read?per_page=101')->assertUnprocessable(); $cut=$this->getJson('/api/stock-read')->assertOk()->json('snapshot'); $this->getJson('/api/stock-read?warehouse_id=12&snapshot='.urlencode($cut))->assertConflict(); $this->getJson('/api/stock-read?tenant_id=88')->assertUnprocessable(); $this->postJson('/api/stock-read',['quantity'=>1])->assertStatus(405); $this->withHeader('X-SGF-Context','old-context')->getJson('/api/stock-read')->assertConflict(); $this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['sibling'],'unit_id'=>$this->f['siblingUnit'],'module_id'=>'stock'])->assertForbidden(); $this->actingAs($this->f['reader'])->withSession(['auth_version'=>$this->f['reader']->auth_version]); $this->getJson('/api/stock-read')->assertForbidden(); } public function test_snapshot_invalidation_context_revocation_and_unavailable_source(): void { $this->fixture(30); $cut=$this->getJson('/api/stock-read')->assertOk()->json('snapshot'); $this->getJson('/api/stock-read?page=2&snapshot='.urlencode($cut))->assertOk()->assertJsonPath('rows.0.stock_item_id',26); DB::table($this->schema.'.stock_movements')->insert(['tenant_id'=>77,'stock_item_id'=>1,'warehouse_id'=>11,'quantity_milli'=>-1000,'movement_type'=>'issue','snapshot'=>'{}','moved_at'=>now()]); $this->getJson('/api/stock-read?page=2&snapshot='.urlencode($cut))->assertConflict(); $new=$this->getJson('/api/stock-read')->assertOk()->assertJsonPath('rows.0.physical_milli','7000')->json('snapshot'); DB::table($this->schema.'.stock_items')->where('id',1)->update(['name'=>'Synthetic renamed']); $this->getJson('/api/stock-read?page=2&snapshot='.urlencode($new))->assertConflict(); $this->getJson('/api/stock-read?snapshot=invalid')->assertConflict(); $this->getJson('/api/stock-read')->assertOk(); $binding=config('stock_read.synthetic_sources.'.$this->key); config(['stock_read.synthetic_sources.'.$this->key=>array_replace($binding,['warehouse_ids'=>[999999]])]); $this->getJson('/api/stock-read')->assertStatus(503); config(['stock_read.synthetic_sources.'.$this->key=>array_replace($binding,['schema'=>'sgf_stock_read_000000000000'])]); $this->getJson('/api/stock-read')->assertStatus(503)->assertJsonPath('status','unavailable')->assertJsonMissingPath('exception'); config(['stock_read.synthetic_sources.'.$this->key=>$binding]); $this->getJson('/api/stock-read')->assertOk(); config(['stock_read.synthetic_sources.'.$this->key=>null]); $this->getJson('/api/stock-read')->assertStatus(503)->assertJsonPath('status','unavailable')->assertJsonMissingPath('rows'); DB::table('permission_grants')->where('user_id',$this->f['manager']->id)->where('permission','stock.view')->delete(); $this->getJson('/api/stock-read')->assertForbidden(); } public function test_volume_pages_query_count_and_exact_coverage(): void { $report=['date'=>'2026-11-02','kind'=>'new_inventory_reader_not_association_rebenchmark','profiles'=>[], 'source_composition'=>'SGF adapter and common reader in isolated regression only; Purchasing HTTP composition not yet connected', 'indexes'=>'Primary keys only; no index experiment','fixtures'=>'New QA-only schemas and identities retained; no reset or protected-record changes']; $stats=null;DB::listen(function ($q) use (&$stats) {if($stats!==null){$stats['queries']++;$stats['database_ms']+=$q->time;}}); foreach ([1000,5000,10000] as $n) { $this->fixture($n); foreach (['items','warehouses','balances','movements'] as $view) foreach ([25,100] as $size) { $url='/api/stock-read?view='.$view.'&per_page='.$size; $this->getJson($url)->assertOk();$samples=[]; for($i=0;$i<3;$i++) { gc_collect_cycles();memory_reset_peak_usage();$memory=memory_get_usage(false);$stats=['queries'=>0,'database_ms'=>0.0];$start=hrtime(true); $r=$this->getJson($url);$elapsed=(hrtime(true)-$start)/1e6;$sample=$stats;$stats=null; $r->assertOk()->assertJsonPath('pagination.total',match($view){'items'=>$n+1,'warehouses'=>2,'balances'=>$n,'movements'=>$n*2}); $sample+=['elapsed_ms'=>round($elapsed,3),'incremental_php_bytes'=>max(0,memory_get_peak_usage(false)-$memory),'peak_php_bytes'=>memory_get_peak_usage(false),'response_bytes'=>strlen($r->getContent())];$samples[]=$sample; } $counts=array_unique(array_column($samples,'queries'));$this->assertCount(1,$counts);$this->assertLessThan(35,$counts[0]); $times=array_column($samples,'elapsed_ms');sort($times); $report['profiles'][]=['records'=>$n,'view'=>$view,'page_size'=>$size,'median_ms'=>$times[1],'samples'=>$samples]; } $cut=null;$ids=[]; for($page=1;$page<=ceil($n/100);$page++){ $url='/api/stock-read?view=balances&per_page=100&page='.$page.($cut?'&snapshot='.urlencode($cut):''); $r=$this->getJson($url)->assertOk();$cut??=$r->json('snapshot');$ids=array_merge($ids,array_column($r->json('rows'),'stock_item_id')); } $this->assertSame(range(1,$n),$ids); } $output=getenv('SGF_STOCK_READ_OUTPUT'); if($output){ $this->assertSame(realpath(base_path('..')),realpath(dirname($output))); $report['source_sha256']=hash_file('sha256',base_path('packages/inventory-read/src/InventoryReader.php')); $report['controller_sha256']=hash_file('sha256',base_path('app/Modules/Inventory/StockReadController.php')); file_put_contents($output,json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR).PHP_EOL); } } public function test_purchasing_composition_matches_authenticated_sgf_pages(): void { $input=getenv('STOCK_READ_NATIVE_PROOF'); if(!$input)$this->markTestSkipped('Use the isolated two-composition launcher.'); $private=realpath(base_path('../private'));$file=realpath($input); $this->assertTrue($file!==false&&str_starts_with($file,$private.DIRECTORY_SEPARATOR)); $native=json_decode(file_get_contents($file),true,512,JSON_THROW_ON_ERROR); $this->assertTrue($native['native_roles_and_profiles_revalidated']);$this->assertTrue($native['warehouse_isolation']); $this->assertCount(12,$native['cases']);$this->fixture(100);$cases=[]; foreach($native['cases'] as $query=>$expected){ $r=$this->getJson('/api/stock-read?'.$query)->assertOk(); $actual=hash('sha256',json_encode([$r->json('rows'),$r->json('pagination')],JSON_THROW_ON_ERROR)); $this->assertSame($expected['sha256'],$actual,'Both authenticated host adapters must return identical scoped rows and pagination'); $this->assertSame($expected['total'],$r->json('pagination.total')); $cases[$query]=['sha256'=>$actual,'total'=>$expected['total'],'equivalent'=>true]; } $this->getJson('/api/stock-read?snapshot='.urlencode($native['native_snapshot']))->assertConflict(); $output=getenv('SGF_STOCK_READ_OUTPUT');$this->assertSame(realpath(base_path('..')),realpath(dirname($output))); file_put_contents($output,json_encode(['kind'=>'two_authenticated_http_kernels','cases'=>$cases,'cross_host_token_refused'=>true, 'reader_sha256'=>hash_file('sha256',base_path('packages/inventory-read/src/InventoryReader.php')), 'session_sha256'=>hash_file('sha256',base_path('packages/inventory-read/src/InventoryReadSession.php')), 'native_package_manifest_sha256'=>$native['package_manifest_sha256'],'network_login_browser_tested'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); } public function test_native_receipt_book_is_observed_without_a_second_stock_effect(): void { $root=realpath((string)getenv('STOCK_RECEIPT_NATIVE_ROOT')); if(!$root)$this->markTestSkipped('Use the receipt integration launcher.'); $this->assertStringStartsWith(realpath(base_path('../private')).DIRECTORY_SEPARATOR,$root); $this->assertMatchesRegularExpression('/^stock-receipt-[a-f0-9]{12}$/D',basename($root)); $native=json_decode(file_get_contents($root.'/receipt-results.json'),true,512,JSON_THROW_ON_ERROR); $source=json_decode(file_get_contents($root.'/source-connection.json'),true,512,JSON_THROW_ON_ERROR); $this->assertTrue($native['fault_atomic_and_recovered']);$this->assertTrue($native['concurrency']['one_movement']); $this->fixture(1); config(['database.connections.stock_receipt_source'=>$source,'stock_read.synthetic_sources.'.$this->key=>[ 'connection'=>'stock_receipt_source','schema'=>'public','source_tenant_id'=>$native['link']['tenant'],'warehouse_ids'=>[$native['link']['warehouse']],'append_only'=>true]]); DB::purge('stock_receipt_source');$connection=DB::connection('stock_receipt_source'); $sourceBefore=$connection->table('stock_movements')->count();$localBefore=DB::table($this->schema.'.stock_movements')->count(); $r=$this->getJson('/api/stock-read')->assertOk()->assertJsonPath('pagination.total',1) ->assertJsonPath('rows.0.physical_milli','4000')->assertJsonPath('rows.0.available_milli',null)->assertJsonPath('capabilities.write',false); $this->assertSame($native['link']['item'],$r->json('rows.0.stock_item_id')); $this->getJson('/api/stock-read?view=movements')->assertOk()->assertJsonPath('pagination.total',1) ->assertJsonPath('rows.0.id',$native['link']['movement'])->assertJsonPath('rows.0.source_reference',$native['link']['reference']); $this->getJson('/api/stock-read?warehouse_id=9999999')->assertForbidden(); try{$connection->statement('DELETE FROM public.stock_movements WHERE false');$this->fail('Source reader must not have write access');} catch(\Illuminate\Database\QueryException $e){$this->assertContains($e->errorInfo[0],['25006','42501']);} $this->getJson('/api/stock-read')->assertOk(); $this->assertSame($sourceBefore,$connection->table('stock_movements')->count());$this->assertSame($localBefore,DB::table($this->schema.'.stock_movements')->count()); DB::purge('stock_receipt_source');config(['database.connections.stock_receipt_source'=>array_replace($source,['password'=>'synthetic-invalid-password'])]); $this->getJson('/api/stock-read')->assertStatus(503)->assertJsonPath('status','unavailable')->assertJsonMissingPath('exception')->assertJsonMissingPath('rows'); DB::purge('stock_receipt_source');config(['database.connections.stock_receipt_source'=>$source]); $this->getJson('/api/stock-read')->assertOk(); $output=getenv('SGF_STOCK_READ_OUTPUT');$this->assertSame(realpath(base_path('..')),realpath(dirname($output))); file_put_contents($output,json_encode(['kind'=>'native_receipt_to_shared_writer_to_sgf_read','physical_milli'=>'4000','rejected_milli'=>'2000', 'same_native_movement_observed'=>true,'source_write_refused'=>true,'native_book_unchanged_by_read'=>true,'sgf_book_unchanged_by_read'=>true,'source_connection_failure_503_and_recovery'=>true, 'source_read_role'=>$source['username'],'writer_manifest_sha256'=>$native['writer_manifest_sha256'],'no_sgf_receipt_command_or_ui'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); DB::disconnect('stock_receipt_source'); } public function test_native_return_book_is_observed_without_copy_or_write():void { $root=realpath((string)getenv('STOCK_RETURN_NATIVE_ROOT'));if(!$root)$this->markTestSkipped('Use the native return observer launcher.'); $this->assertStringStartsWith(realpath(base_path('../private')).DIRECTORY_SEPARATOR,$root);$this->assertMatchesRegularExpression('/^stock-receipt-[a-f0-9]{12}$/D',basename($root)); $native=json_decode(file_get_contents($root.'/return-results.json'),true,512,JSON_THROW_ON_ERROR);$source=json_decode(file_get_contents($root.'/source-connection.json'),true,512,JSON_THROW_ON_ERROR);$this->fixture(1); config(['database.connections.stock_receipt_source'=>$source,'stock_read.synthetic_sources.'.$this->key=>['connection'=>'stock_receipt_source','schema'=>'public','source_tenant_id'=>$native['tenant'],'warehouse_ids'=>[$native['warehouse']],'append_only'=>true]]);DB::purge('stock_receipt_source');$connection=DB::connection('stock_receipt_source'); $before=$connection->table('stock_movements')->where('tenant_id',$native['tenant'])->count();$local=DB::table($this->schema.'.stock_movements')->count(); $this->getJson('/api/stock-read')->assertOk()->assertJsonPath('rows.0.physical_milli','8000')->assertJsonPath('rows.0.stock_item_id',$native['item'])->assertJsonPath('capabilities.write',false); $moves=$this->getJson('/api/stock-read?view=movements')->assertOk()->assertJsonPath('pagination.total',3)->json('rows');$returned=collect($moves)->firstWhere('id',$native['return_movement']);$this->assertNotNull($returned);$this->assertSame('3000',$returned['quantity_milli']);$this->assertSame('workshop_return_in',$returned['movement_type']); $this->getJson('/api/stock-read?warehouse_id=9999999')->assertForbidden();try{$connection->statement('DELETE FROM public.stock_movements WHERE false');$this->fail('Read-only role accepted write');}catch(\Illuminate\Database\QueryException $e){$this->assertContains($e->errorInfo[0],['25006','42501']);} $this->assertSame(3,$before);$this->assertSame($before,$connection->table('stock_movements')->where('tenant_id',$native['tenant'])->count());$this->assertSame($local,DB::table($this->schema.'.stock_movements')->count()); $output=getenv('SGF_STOCK_READ_OUTPUT');$this->assertSame(realpath(base_path('..')),realpath(dirname($output)));file_put_contents($output,json_encode(['kind'=>'native_return_same_book_sgf_observer','physical_milli'=>'8000','native_movements'=>3,'same_return_movement'=>true,'observed_return_movement_id'=>$native['return_movement'],'native_fixture_sha256'=>hash_file('sha256',$root.'/return-results.json'),'source_write_refused'=>true,'no_local_copy_or_second_effect'=>true,'return_manifest_sha256'=>$native['shared_return_manifest_sha256']],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); } }