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); $warnings1 = $svc->extractForPolicyData($item, $policyData); $warnings2 = $svc->extractForPolicyData($item, $policyData); // re-run, should be idempotent expect($warnings1)->toBeArray()->toBeEmpty(); expect($warnings2)->toBeArray()->toBeEmpty(); $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 recording warnings (no 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 ], ]; Log::spy(); $svc = app(DependencyExtractionService::class); $warnings = $svc->extractForPolicyData($item, $policyData); expect($warnings)->toBeArray()->toHaveCount(1); expect($warnings[0]['type'] ?? null)->toBe('unsupported_reference'); expect($warnings[0]['policy_id'] ?? null)->toBe($item->external_id); expect(InventoryLink::query()->count())->toBe(0); Log::shouldHaveReceived('info') ->withArgs(fn (string $message, array $context) => $message === 'Unsupported reference shape encountered' && ($context['type'] ?? null) === 'unsupported_reference' && ($context['policy_id'] ?? null) === $item->external_id) ->once(); });