assertTrue($reflection->isAbstract()); } public function test_base_extends_runtime_exception(): void { $this->assertTrue(is_subclass_of(FormBindingApplicatorException::class, RuntimeException::class)); } public function test_schema_config_exception_constructor_and_reason_code(): void { $e = new FormBindingSchemaConfigException( submissionId: '01HX1234567890ABCDEFGHJKMN', message: 'schema null', ); $this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId); $this->assertSame('schema null', $e->getMessage()); $this->assertSame('schema_config_error', $e->reasonCode()); } public function test_infra_exception_constructor_and_reason_code(): void { $e = new FormBindingInfraException( submissionId: '01HX1234567890ABCDEFGHJKMN', message: 'no transaction', ); $this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId); $this->assertSame('no transaction', $e->getMessage()); $this->assertSame('temporary_error', $e->reasonCode()); } public function test_data_integrity_exception_constructor_and_reason_code(): void { $e = new FormBindingDataIntegrityException( submissionId: '01HX1234567890ABCDEFGHJKMN', message: 'fk violation', ); $this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId); $this->assertSame('fk violation', $e->getMessage()); $this->assertSame('data_integrity_error', $e->reasonCode()); } public function test_timeout_exception_constructor_and_inherited_reason_code(): void { $e = new FormBindingApplicatorTimeoutException( submissionId: '01HX1234567890ABCDEFGHJKMN', message: 'deadline exceeded after 5s', ); $this->assertSame('01HX1234567890ABCDEFGHJKMN', $e->submissionId); $this->assertSame('deadline exceeded after 5s', $e->getMessage()); // Inherited from FormBindingInfraException — no override. $this->assertSame('temporary_error', $e->reasonCode()); } public function test_timeout_extends_infra(): void { $this->assertTrue(is_subclass_of( FormBindingApplicatorTimeoutException::class, FormBindingInfraException::class, )); } public function test_all_concrete_subclasses_extend_base(): void { $concreteSubclasses = [ FormBindingSchemaConfigException::class, FormBindingInfraException::class, FormBindingDataIntegrityException::class, FormBindingApplicatorTimeoutException::class, ]; foreach ($concreteSubclasses as $class) { $this->assertTrue( is_subclass_of($class, FormBindingApplicatorException::class), "Class {$class} must extend FormBindingApplicatorException", ); } } public function test_constructor_accepts_previous_throwable(): void { $cause = new RuntimeException('original'); $e = new FormBindingInfraException( submissionId: '01HX', message: 'wrapper', previous: $cause, ); $this->assertSame($cause, $e->getPrevious()); } public function test_identity_match_invariant_violation_is_not_in_hierarchy(): void { $this->assertFalse(is_subclass_of( IdentityMatchInvariantViolation::class, FormBindingApplicatorException::class, )); } public function test_identity_match_invariant_violation_is_domain_exception(): void { $this->assertTrue(is_subclass_of( IdentityMatchInvariantViolation::class, DomainException::class, )); } }