assertSame('schema_config_error', $code); } public function test_classifies_infra_exception(): void { $code = FormBindingExceptionClassifier::classify( new FormBindingInfraException(submissionId: '01HX', message: 'x'), ); $this->assertSame('temporary_error', $code); } public function test_classifies_timeout_as_temporary_error(): void { // Subclass dispatch — Timeout extends Infra, inherits reasonCode. $code = FormBindingExceptionClassifier::classify( new FormBindingApplicatorTimeoutException(submissionId: '01HX', message: 'deadline'), ); $this->assertSame('temporary_error', $code); } public function test_classifies_data_integrity_exception(): void { $code = FormBindingExceptionClassifier::classify( new FormBindingDataIntegrityException(submissionId: '01HX', message: 'fk'), ); $this->assertSame('data_integrity_error', $code); } public function test_classifies_arbitrary_runtime_exception_as_unknown(): void { $code = FormBindingExceptionClassifier::classify(new RuntimeException('boom')); $this->assertSame('unknown_error', $code); } public function test_classifies_identity_match_invariant_violation_as_unknown(): void { // IdentityMatchInvariantViolation is intentionally NOT in the // FormBindingApplicatorException hierarchy (per RFC-WS-6 §Q2 — the // listener that throws it runs outside the binding-applicator // pipeline). It falls through to 'unknown_error' here, which is // the right response-shape because users cannot meaningfully act // on it; admins triage via GlitchTip. $code = FormBindingExceptionClassifier::classify(new IdentityMatchInvariantViolation('x')); $this->assertSame('unknown_error', $code); } }