create(); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); $startCount = DB::table('activity_log') ->where('tenant_id', $tenant->id) ->count(); // Spawn 5 concurrent processes each inserting into activity_log for the same tenant. // Without advisory lock, concurrent reads of prev_hash return the same value // → multiple rows hash to the same prev → chain branch → validator fails. $pids = []; for ($i = 0; $i < 5; $i++) { $pid = pcntl_fork(); if ($pid === 0) { // Child: own DB connection, own transaction DB::reconnect(); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); DB::table('activity_log')->insert([ 'tenant_id' => $tenant->id, 'event' => 'deal.created', 'context' => json_encode(['worker' => $i]), 'created_at' => now(), ]); exit(0); } $pids[] = $pid; } foreach ($pids as $pid) { pcntl_waitpid($pid, $status); } $rows = DB::table('activity_log') ->where('tenant_id', $tenant->id) ->orderBy('id') ->get(['id', 'log_hash']); expect($rows->count())->toBe($startCount + 5); // Run the chain validator; it should find no mismatches (after migration). $exitCode = $this->artisan('audit:verify-chains')->run(); expect($exitCode)->toBe(0); } )->skip(! function_exists('pcntl_fork'), 'pcntl required for race-condition test (not available on Windows)'); it('audit_chain_hash holds pg_advisory_xact_lock on the partition OID during INSERT', function (): void { $tenant = Tenant::factory()->create(); DB::statement('SET LOCAL app.current_tenant_id = '.$tenant->id); // Resolve the OID of the current-month activity_log partition (or parent). $partitionName = 'activity_log_y'.date('Y').'_m'.date('m'); $oid = DB::selectOne( "SELECT COALESCE( (SELECT c.oid FROM pg_class c WHERE c.relname = ?), (SELECT c.oid FROM pg_class c WHERE c.relname = 'activity_log') ) AS oid", [$partitionName] )?->oid; expect($oid)->not->toBeNull('Could not resolve partition/parent OID'); // Compute the lock key using the same formula as the trigger: // ('x' || lpad(to_hex(TG_RELID::int), 16, '0'))::bit(64)::bigint $lockKeyRow = DB::selectOne( "SELECT ('x' || lpad(to_hex(?::int), 16, '0'))::bit(64)::bigint AS lock_key", [(int) $oid] ); $lockKey = $lockKeyRow?->lock_key; expect($lockKey)->not->toBeNull(); // Wrap an INSERT in a transaction and check pg_locks DURING that transaction. $lockHeld = false; DB::transaction(function () use ($tenant, $lockKey, &$lockHeld): void { DB::table('activity_log')->insert([ 'tenant_id' => $tenant->id, 'event' => 'deal.created', 'context' => json_encode(['test' => 'advisory_lock_check']), 'created_at' => now(), ]); // pg_advisory_xact_lock releases at END of transaction — still held here. $held = DB::selectOne( 'SELECT EXISTS ( SELECT 1 FROM pg_locks WHERE locktype = \'advisory\' AND classid = (? >> 32)::int AND objid = (? & x\'ffffffff\'::bigint)::int AND granted = true AND pid = pg_backend_pid() ) AS held', [(int) $lockKey, (int) $lockKey] ); $lockHeld = (bool) ($held->held ?? false); }); expect($lockHeld)->toBeTrue( 'pg_advisory_xact_lock was not observed in pg_locks during the INSERT transaction. ' .'This means the migration has not been applied or the lock key formula is wrong.' ); });