create(); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $policyData = [ 'id' => $item->external_id, 'assignments' => [ ['target' => ['groupId' => 'group-1']], ['target' => ['groupId' => 'group-2']], ], 'roleScopeTagIds' => ['scope-tag-1', 'scope-tag-2'], ]; $svc = app(DependencyExtractionService::class); $svc->extractForPolicyData($item, $policyData); $svc->extractForPolicyData($item, $policyData); // re-run, should be idempotent $edges = InventoryLink::query()->where('tenant_id', $tenant->getKey())->get(); expect($edges)->toHaveCount(4); // Ensure uniqueness by tuple (source, target, type) $tuples = $edges->map(fn ($e) => implode('|', [ $e->source_type, $e->source_id, $e->target_type, (string) $e->target_id, $e->relationship_type, ]))->unique(); expect($tuples->count())->toBe(4); }); it('handles unsupported references by creating missing edges', function () { $tenant = Tenant::factory()->create(); /** @var InventoryItem $item */ $item = InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'external_id' => (string) Str::uuid(), ]); $policyData = [ 'id' => $item->external_id, 'assignments' => [ ['target' => ['filterId' => 'filter-only-no-group']], // no groupId shape → missing ], ]; $svc = app(DependencyExtractionService::class); $svc->extractForPolicyData($item, $policyData); $edge = InventoryLink::query()->first(); expect($edge)->not->toBeNull(); expect($edge->target_type)->toBe('missing'); expect($edge->target_id)->toBeNull(); });