seedSchemaWithTwoFields(); $service = app(FormFieldConditionalLogicService::class); // A depends on B. $service->replaceLogic($fieldA, [ 'operator' => 'all', 'children' => [ ['field_slug' => $fieldB->slug, 'operator' => 'equals', 'value' => 'y'], ], ]); // Proposing B depends on A would close the A → B → A cycle. $this->expectException(CyclicDependencyException::class); $service->assertNoCycles($fieldB, [ 'operator' => 'all', 'children' => [ ['field_slug' => $fieldA->slug, 'operator' => 'equals', 'value' => 'x'], ], ]); } public function test_three_node_cycle_rejected(): void { [$schema, $fieldA, $fieldB, $fieldC] = $this->seedSchemaWithThreeFields(); $service = app(FormFieldConditionalLogicService::class); // A → B, B → C $service->replaceLogic($fieldA, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldB->slug, 'operator' => 'equals', 'value' => 'y']], ]); $service->replaceLogic($fieldB, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldC->slug, 'operator' => 'equals', 'value' => 'y']], ]); // Proposing C → A closes A → B → C → A. $this->expectException(CyclicDependencyException::class); $service->assertNoCycles($fieldC, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldA->slug, 'operator' => 'equals', 'value' => 'x']], ]); } public function test_diamond_is_accepted_no_cycle(): void { [$schema, $fieldA, $fieldB, $fieldC] = $this->seedSchemaWithThreeFields(); $service = app(FormFieldConditionalLogicService::class); // A → B and C → B (two fields depend on the same field, no cycle). $service->replaceLogic($fieldA, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldB->slug, 'operator' => 'equals', 'value' => 'y']], ]); $service->assertNoCycles($fieldC, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldB->slug, 'operator' => 'equals', 'value' => 'y']], ]); $this->expectNotToPerformAssertions(); } public function test_self_reference_rejected(): void { [$schema, $fieldA] = $this->seedSchemaWithTwoFields(); $service = app(FormFieldConditionalLogicService::class); $this->expectException(CyclicDependencyException::class); $service->assertNoCycles($fieldA, [ 'operator' => 'all', 'children' => [['field_slug' => $fieldA->slug, 'operator' => 'equals', 'value' => 'x']], ]); } /** @return array{0:FormSchema,1:FormField,2:FormField} */ private function seedSchemaWithTwoFields(): array { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $fieldA = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'a']); $fieldB = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'b']); return [$schema, $fieldA, $fieldB]; } /** @return array{0:FormSchema,1:FormField,2:FormField,3:FormField} */ private function seedSchemaWithThreeFields(): array { $org = Organisation::factory()->create(); $schema = FormSchema::factory()->create(['organisation_id' => $org->id]); $fieldA = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'a']); $fieldB = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'b']); $fieldC = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'c']); return [$schema, $fieldA, $fieldB, $fieldC]; } }