create(); $user = User::factory()->for($tenant)->create(); app(OperationsLogger::class)->record( tenantId: $tenant->id, userId: $user->id, entityType: 'project', entityId: 42, event: 'project.created', payloadBefore: null, payloadAfter: ['name' => 'X', 'limit' => 10], ip: '1.2.3.4', userAgent: 'UA', ); $row = DB::table('tenant_operations_log')->latest('id')->first(); expect($row->event)->toBe('project.created') ->and($row->entity_type)->toBe('project') ->and((int) $row->entity_id)->toBe(42) ->and((int) $row->user_id)->toBe($user->id) ->and((int) $row->tenant_id)->toBe($tenant->id) ->and((string) $row->ip_address)->toBe('1.2.3.4') ->and(json_decode($row->payload_after, true))->toBe(['name' => 'X', 'limit' => 10]) ->and($row->payload_before)->toBeNull(); }); it('allows system actor (user_id NULL) for system operations', function () { $tenant = Tenant::factory()->create(); $before = DB::table('tenant_operations_log')->where('tenant_id', $tenant->id)->count(); app(OperationsLogger::class)->record( tenantId: $tenant->id, userId: null, entityType: 'webhook_settings', entityId: null, event: 'webhook_settings.system_purge', payloadBefore: null, payloadAfter: null, ip: null, userAgent: null, ); expect(DB::table('tenant_operations_log')->where('tenant_id', $tenant->id)->count())->toBe($before + 1); }); it('append-only: UPDATE blocked by audit_block_mutation', function () { $tenant = Tenant::factory()->create(); app(OperationsLogger::class)->record( tenantId: $tenant->id, userId: null, entityType: 'project', entityId: 1, event: 'project.created', payloadBefore: null, payloadAfter: ['x' => 1], ip: null, userAgent: null, ); $id = (int) DB::table('tenant_operations_log')->latest('id')->value('id'); expect(fn () => DB::table('tenant_operations_log')->where('id', $id)->update(['event' => 'x'])) ->toThrow(QueryException::class); });