From 15a97b9507a65489fc538748913030add0eb240c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 21:38:52 +0000 Subject: [PATCH 1/4] Distinguish input vs runtime semantic validation failures Semantic variable validation could fail at any point in a metamorphosis chain, but all failures raised the same SemanticVariableException with no indication of where they occurred. The first metamorphosis validates the data flowing in from the user-supplied input object (an input error), while any later metamorphosis validates already-validated state (a runtime error signalling an internal inconsistency). These are semantically different and callers should be able to tell them apart. Add two subtypes of SemanticVariableException - InputSemanticVariableException and RuntimeSemanticVariableException - so the distinction is carried by type. Becoming, which is the only component aware of a transformation's position in the chain, refines the base exception into the appropriate subtype on catch, preserving the original as the previous exception. Catching the base type still catches both, keeping existing behaviour intact. --- src/Becoming.php | 14 ++++ .../InputSemanticVariableException.php | 18 +++++ .../RuntimeSemanticVariableException.php | 19 ++++++ src/Exception/SemanticVariableException.php | 13 +++- tests/BecomingTest.php | 68 +++++++++++++++++++ .../InputSemanticVariableExceptionTest.php | 39 +++++++++++ .../RuntimeSemanticVariableExceptionTest.php | 39 +++++++++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/Exception/InputSemanticVariableException.php create mode 100644 src/Exception/RuntimeSemanticVariableException.php create mode 100644 tests/Exception/InputSemanticVariableExceptionTest.php create mode 100644 tests/Exception/RuntimeSemanticVariableExceptionTest.php diff --git a/src/Becoming.php b/src/Becoming.php index 8c361fdb..8d921a60 100644 --- a/src/Becoming.php +++ b/src/Becoming.php @@ -4,6 +4,9 @@ namespace Be\Framework; +use Be\Framework\Exception\InputSemanticVariableException; +use Be\Framework\Exception\RuntimeSemanticVariableException; +use Be\Framework\Exception\SemanticVariableException; use Be\Framework\SemanticLog\Logger; use Be\Framework\SemanticLog\LoggerInterface; use Be\Framework\SemanticVariable\SemanticValidator; @@ -51,13 +54,24 @@ public function __invoke(object $input): object { $chainId = $this->logger->openChain($input); $current = $input; + $isFirst = true; try { // Being reveals its becoming, then becomes it while ($nextForm = $this->being->willBe($current)) { $current = $this->being->metamorphose($current, $nextForm); + $isFirst = false; } } catch (Throwable $e) { + // Refine semantic validation failures by their position in the chain: + // the first metamorphosis validates incoming input (input error), + // any later one validates already-validated state (runtime error). + if ($e instanceof SemanticVariableException) { + $e = $isFirst + ? new InputSemanticVariableException($e->getErrors(), $e) + : new RuntimeSemanticVariableException($e->getErrors(), $e); + } + try { $this->logger->closeChain(null, $chainId, $e); } catch (Throwable) { diff --git a/src/Exception/InputSemanticVariableException.php b/src/Exception/InputSemanticVariableException.php new file mode 100644 index 00000000..cb3a16b2 --- /dev/null +++ b/src/Exception/InputSemanticVariableException.php @@ -0,0 +1,18 @@ + $exception->getMessage(), @@ -31,7 +40,7 @@ public function __construct( ? $errorMessages[0] : 'Multiple semantic validation errors: ' . implode(', ', $errorMessages); - parent::__construct($message); + parent::__construct($message, 0, $previous); } /** diff --git a/tests/BecomingTest.php b/tests/BecomingTest.php index d5164593..aa39b582 100644 --- a/tests/BecomingTest.php +++ b/tests/BecomingTest.php @@ -8,7 +8,9 @@ use Be\Framework\Attribute\Validate; use Be\Framework\Exception\BeMatchException; use Be\Framework\Exception\ConflictingParameterAttributes; +use Be\Framework\Exception\InputSemanticVariableException; use Be\Framework\Exception\MissingParameterAttribute; +use Be\Framework\Exception\RuntimeSemanticVariableException; use Be\Framework\Exception\SemanticVariableException; use Be\Framework\Exception\UnbecomingException; use Be\Framework\SemanticVariable\Errors; @@ -183,6 +185,37 @@ public function testSemanticValidationFailure(): void ($this->becoming)($input); } + public function testFirstMetamorphosisFailsWithInputException(): void + { + // Validation failing on the first metamorphosis is an input error + $input = new BecomingTestSemanticInvalid('invalid-email'); + + try { + ($this->becoming)($input); + $this->fail('Expected InputSemanticVariableException'); + } catch (InputSemanticVariableException $e) { + // The refined input subtype is still a SemanticVariableException + $this->assertInstanceOf(SemanticVariableException::class, $e); + $this->assertTrue($e->getErrors()->hasErrors()); + } + } + + public function testLaterMetamorphosisFailsWithRuntimeException(): void + { + // The first transformation succeeds; the second fails validation, + // which is a runtime error rather than an input error. + $input = new BecomingTestRuntimeStart('seed'); + + try { + ($this->becoming)($input); + $this->fail('Expected RuntimeSemanticVariableException'); + } catch (RuntimeSemanticVariableException $e) { + $this->assertInstanceOf(SemanticVariableException::class, $e); + $this->assertNotInstanceOf(InputSemanticVariableException::class, $e); + $this->assertTrue($e->getErrors()->hasErrors()); + } + } + public function testTypeMatchingFailureWithFallback(): void { // This test covers lines 106-107 in Being.php performTypeMatching @@ -614,6 +647,41 @@ public function __construct( } } +// Runtime semantic validation test fixtures: a two-step chain where the first +// transformation passes validation and the second produces invalid data. +#[Be(BecomingTestRuntimeMiddle::class)] +final class BecomingTestRuntimeStart +{ + public function __construct( + public readonly string $value, // no Value semantic class - first step passes + ) { + } +} + +#[Be(BecomingTestRuntimeFailingTarget::class)] +final class BecomingTestRuntimeMiddle +{ + public readonly string $email; + + public function __construct( + #[Input] + string $value, // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter + ) { + // Produce an invalid email for the next metamorphosis to validate + $this->email = 'invalid-email'; + } +} + +final class BecomingTestRuntimeFailingTarget +{ + public function __construct( + #[Input] + #[Validate('Email')] + public readonly string $email, // fails semantic validation on the second step + ) { + } +} + // UnbecomingException test fixtures #[Be([BecomingTestRejectionTarget::class, BecomingTestRejectionFallback::class])] final class BecomingTestRejectionInput diff --git a/tests/Exception/InputSemanticVariableExceptionTest.php b/tests/Exception/InputSemanticVariableExceptionTest.php new file mode 100644 index 00000000..c41b19be --- /dev/null +++ b/tests/Exception/InputSemanticVariableExceptionTest.php @@ -0,0 +1,39 @@ +assertInstanceOf(SemanticVariableException::class, $exception); + } + + public function testGetErrorsAndMessage(): void + { + $errors = new Errors([new DomainException('Invalid email format')]); + $exception = new InputSemanticVariableException($errors); + + $this->assertSame('Invalid email format', $exception->getMessage()); + $this->assertSame($errors, $exception->getErrors()); + } + + public function testPreviousIsChained(): void + { + $errors = new Errors([new DomainException('Invalid input')]); + $previous = new SemanticVariableException($errors); + + $exception = new InputSemanticVariableException($errors, $previous); + + $this->assertSame($previous, $exception->getPrevious()); + } +} diff --git a/tests/Exception/RuntimeSemanticVariableExceptionTest.php b/tests/Exception/RuntimeSemanticVariableExceptionTest.php new file mode 100644 index 00000000..86dd476b --- /dev/null +++ b/tests/Exception/RuntimeSemanticVariableExceptionTest.php @@ -0,0 +1,39 @@ +assertInstanceOf(SemanticVariableException::class, $exception); + } + + public function testGetErrorsAndMessage(): void + { + $errors = new Errors([new DomainException('Invalid email format')]); + $exception = new RuntimeSemanticVariableException($errors); + + $this->assertSame('Invalid email format', $exception->getMessage()); + $this->assertSame($errors, $exception->getErrors()); + } + + public function testPreviousIsChained(): void + { + $errors = new Errors([new DomainException('Internal inconsistency')]); + $previous = new SemanticVariableException($errors); + + $exception = new RuntimeSemanticVariableException($errors, $previous); + + $this->assertSame($previous, $exception->getPrevious()); + } +} From 46b2ec3f3b06c589a393a7fac8ffbba9bde367f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 13:34:48 +0000 Subject: [PATCH 2/4] Harden tests for input/runtime validation classification The exception-type contract was only pinned by direct-construction unit tests and by instanceof checks in the integration tests, leaving regression gaps a green suite would not catch: - The cause chain (getPrevious) through Becoming's rewrap was never asserted on the real metamorphosis path, so dropping the $previous argument would silently lose the original validation site. - The array/branching first step (performTypeMatching) had no test proving a validation failure there is classified as an input error. - The chain-close log now records the refined subtype FQCN; nothing pinned the "refine before closeChain" ordering. Add getPrevious/getErrors assertions to the two integration tests, a branching first-step input-classification test, and a chain-close logging test that inspects the emitted becoming_close payload via a real SemanticLogger. --- tests/BecomingTest.php | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/BecomingTest.php b/tests/BecomingTest.php index aa39b582..aa386ea1 100644 --- a/tests/BecomingTest.php +++ b/tests/BecomingTest.php @@ -13,9 +13,11 @@ use Be\Framework\Exception\RuntimeSemanticVariableException; use Be\Framework\Exception\SemanticVariableException; use Be\Framework\Exception\UnbecomingException; +use Be\Framework\SemanticLog\Logger; use Be\Framework\SemanticVariable\Errors; use Be\Framework\SemanticVariable\SemanticValidator; use InvalidArgumentException; +use Koriym\SemanticLogger\SemanticLogger; use PHPUnit\Framework\TestCase; use Ray\Di\AbstractModule; use Ray\Di\Di\Inject; @@ -24,7 +26,9 @@ use Ray\InputQuery\Attribute\Input; use RuntimeException; +use function assert; use function filter_var; +use function is_array; use const FILTER_VALIDATE_EMAIL; @@ -197,6 +201,14 @@ public function testFirstMetamorphosisFailsWithInputException(): void // The refined input subtype is still a SemanticVariableException $this->assertInstanceOf(SemanticVariableException::class, $e); $this->assertTrue($e->getErrors()->hasErrors()); + + // The original base exception is preserved as the cause so the real + // validation site stays reachable. Pin it on the Becoming rewrap path + // (not just in the direct-construction unit tests). + $previous = $e->getPrevious(); + $this->assertInstanceOf(SemanticVariableException::class, $previous); + $this->assertNotInstanceOf(InputSemanticVariableException::class, $previous); + $this->assertSame($e->getErrors(), $previous->getErrors()); } } @@ -213,9 +225,59 @@ public function testLaterMetamorphosisFailsWithRuntimeException(): void $this->assertInstanceOf(SemanticVariableException::class, $e); $this->assertNotInstanceOf(InputSemanticVariableException::class, $e); $this->assertTrue($e->getErrors()->hasErrors()); + + // Cause chain preserved through the rewrap, same Errors carried over. + $previous = $e->getPrevious(); + $this->assertInstanceOf(SemanticVariableException::class, $previous); + $this->assertNotInstanceOf(RuntimeSemanticVariableException::class, $previous); + $this->assertSame($e->getErrors(), $previous->getErrors()); + } + } + + public function testBranchingFirstStepFailsWithInputException(): void + { + // The first metamorphosis through the array/branching path (performTypeMatching) + // must classify a validation failure as an input error, just like the linear path. + $input = new BecomingTestSemanticFailureInput('invalid-email'); + + try { + ($this->becoming)($input); + $this->fail('Expected InputSemanticVariableException'); + } catch (InputSemanticVariableException $e) { + $this->assertNotInstanceOf(RuntimeSemanticVariableException::class, $e); + $this->assertTrue($e->getErrors()->hasErrors()); } } + public function testChainCloseLogRecordsRefinedSubtype(): void + { + // The chain-close log must record the refined subtype FQCN, which fixes the + // "refine BEFORE closeChain" ordering in Becoming::__invoke. Use a real + // SemanticLogger so the emitted becoming_close payload can be inspected. + $injector = new Injector(new BecomingTestModule()); + $semanticValidator = new SemanticValidator('MyVendor\\MyApp\\SemanticVariables'); + $becomingArguments = new BecomingArguments($injector, $semanticValidator); + $semanticLogger = new SemanticLogger(); + $logger = new Logger($semanticLogger, $becomingArguments); + $becoming = new Becoming($injector, 'MyVendor\\MyApp', $logger, $becomingArguments); + + try { + $becoming(new BecomingTestSemanticInvalid('invalid-email')); + $this->fail('Expected InputSemanticVariableException'); + } catch (InputSemanticVariableException) { + // expected + } + + $logData = $semanticLogger->toArray(); + assert(is_array($logData['open']) && is_array($logData['open'][0]) && is_array($logData['open'][0]['close'])); + $closeData = $logData['open'][0]['close']; + assert(is_array($closeData) && is_array($closeData['context'])); + + $this->assertSame('becoming_close', $closeData['type']); + $this->assertSame('error', $closeData['context']['exit']); + $this->assertSame(InputSemanticVariableException::class, $closeData['context']['error']); + } + public function testTypeMatchingFailureWithFallback(): void { // This test covers lines 106-107 in Being.php performTypeMatching From cf9cd0a4cd0cb7d2194920ce8d14ce452a26ba79 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:09:40 +0000 Subject: [PATCH 3/4] Resolve span/chain log asymmetry via an origin field When a constructor body threw the base SemanticVariableException, the inner being_error_close span logged that base class while the outer becoming_close span logged the refined subtype, so a single failure was labelled with two different class names across spans. Keep the chain-close log faithful to the actual error class (matching the inner span) and express the input/runtime distinction as a dedicated `origin` field on BecomingCloseContext instead of swapping the class name. Becoming now logs the original base exception plus origin while still throwing the refined subtype to callers, so the type-based contract is unchanged. - Add ORIGIN_INPUT/ORIGIN_RUNTIME and an optional origin field to BecomingCloseContext, emitted only on semantic-validation error exits. - Thread origin through LoggerInterface::closeChain / Logger::closeChain. - Document origin in the becoming-close schema (enum input|runtime, and forbidden on the success branch). - Update the chain-close logging tests to assert the base error class plus the input/runtime origin. --- docs/schemas/becoming-close.json | 8 ++- src/Becoming.php | 11 +++- .../Context/BecomingCloseContext.php | 12 ++++ src/SemanticLog/Logger.php | 3 +- src/SemanticLog/LoggerInterface.php | 4 +- tests/BecomingTest.php | 58 +++++++++++++++---- 6 files changed, 80 insertions(+), 16 deletions(-) diff --git a/docs/schemas/becoming-close.json b/docs/schemas/becoming-close.json index 1a390b13..bd4f6d60 100644 --- a/docs/schemas/becoming-close.json +++ b/docs/schemas/becoming-close.json @@ -13,7 +13,8 @@ "not": { "anyOf": [ { "required": ["error"] }, - { "required": ["message"] } + { "required": ["message"] }, + { "required": ["origin"] } ] } }, @@ -42,6 +43,11 @@ "message": { "type": "string", "description": "Exception message when the chain ended in failure." + }, + "origin": { + "type": "string", + "enum": ["input", "runtime"], + "description": "Provenance of a semantic validation failure: 'input' when the first metamorphosis (incoming input) failed validation, 'runtime' when a later metamorphosis failed. Absent for non-semantic failures." } }, "additionalProperties": false diff --git a/src/Becoming.php b/src/Becoming.php index 8d921a60..27b89b17 100644 --- a/src/Becoming.php +++ b/src/Becoming.php @@ -7,6 +7,7 @@ use Be\Framework\Exception\InputSemanticVariableException; use Be\Framework\Exception\RuntimeSemanticVariableException; use Be\Framework\Exception\SemanticVariableException; +use Be\Framework\SemanticLog\Context\BecomingCloseContext; use Be\Framework\SemanticLog\Logger; use Be\Framework\SemanticLog\LoggerInterface; use Be\Framework\SemanticVariable\SemanticValidator; @@ -66,14 +67,22 @@ public function __invoke(object $input): object // Refine semantic validation failures by their position in the chain: // the first metamorphosis validates incoming input (input error), // any later one validates already-validated state (runtime error). + // Callers receive the refined subtype, while the chain-close log keeps + // the original error class (consistent with the inner span) and carries + // the input/runtime distinction as `origin`. + $loggedException = $e; + $origin = null; if ($e instanceof SemanticVariableException) { + $origin = $isFirst + ? BecomingCloseContext::ORIGIN_INPUT + : BecomingCloseContext::ORIGIN_RUNTIME; $e = $isFirst ? new InputSemanticVariableException($e->getErrors(), $e) : new RuntimeSemanticVariableException($e->getErrors(), $e); } try { - $this->logger->closeChain(null, $chainId, $e); + $this->logger->closeChain(null, $chainId, $loggedException, $origin); } catch (Throwable) { // A failure inside error logging must not mask the original // metamorphosis exception. Swallow the logging error and diff --git a/src/SemanticLog/Context/BecomingCloseContext.php b/src/SemanticLog/Context/BecomingCloseContext.php index f762808f..53a2e500 100644 --- a/src/SemanticLog/Context/BecomingCloseContext.php +++ b/src/SemanticLog/Context/BecomingCloseContext.php @@ -25,17 +25,25 @@ final class BecomingCloseContext extends AbstractContext implements JsonSerializ public const string EXIT_ERROR = 'error'; + public const string ORIGIN_INPUT = 'input'; + + public const string ORIGIN_RUNTIME = 'runtime'; + /** * @param string $exit Exit status for the chain (self::EXIT_SUCCESS|self::EXIT_ERROR). * @param class-string|null $final FQCN of the terminal being, or null if the chain failed. * @param string|null $error FQCN of the thrown exception, or null on success. * @param string|null $message Exception message, or null on success. + * @param string|null $origin Provenance of a semantic validation failure + * (self::ORIGIN_INPUT|self::ORIGIN_RUNTIME), or null when + * the failure is not a semantic validation error. */ public function __construct( public readonly string $exit, public readonly string|null $final = null, public readonly string|null $error = null, public readonly string|null $message = null, + public readonly string|null $origin = null, ) { } @@ -52,6 +60,10 @@ public function jsonSerialize(): array if ($this->error !== null) { $payload['error'] = $this->error; $payload['message'] = $this->message ?? ''; + + if ($this->origin !== null) { + $payload['origin'] = $this->origin; + } } return $payload; diff --git a/src/SemanticLog/Logger.php b/src/SemanticLog/Logger.php index 196ed38d..f1a16cf9 100644 --- a/src/SemanticLog/Logger.php +++ b/src/SemanticLog/Logger.php @@ -81,7 +81,7 @@ public function openChain(object $input): string * `becoming-close.json`, so we refuse to emit one. */ #[Override] - public function closeChain(object|null $final, string $openId, Throwable|null $exception = null): void + public function closeChain(object|null $final, string $openId, Throwable|null $exception = null, string|null $origin = null): void { if ($openId === '') { return; @@ -92,6 +92,7 @@ public function closeChain(object|null $final, string $openId, Throwable|null $e exit: BecomingCloseContext::EXIT_ERROR, error: $exception::class, message: $exception->getMessage(), + origin: $origin, ), $openId); return; diff --git a/src/SemanticLog/LoggerInterface.php b/src/SemanticLog/LoggerInterface.php index 24f9bfcf..b196d73f 100644 --- a/src/SemanticLog/LoggerInterface.php +++ b/src/SemanticLog/LoggerInterface.php @@ -37,8 +37,10 @@ public function openChain(object $input): string; * @param object|null $final Terminal being reached on success; null if the chain failed * @param string $openId Open ID from the corresponding openChain call * @param Throwable|null $exception Exception that ended the chain, or null on success + * @param string|null $origin Provenance of a semantic validation failure ('input'|'runtime'), + * or null when the failure is not a semantic validation error */ - public function closeChain(object|null $final, string $openId, Throwable|null $exception = null): void; + public function closeChain(object|null $final, string $openId, Throwable|null $exception = null, string|null $origin = null): void; /** * Log transformation start diff --git a/tests/BecomingTest.php b/tests/BecomingTest.php index aa386ea1..0ebd5a61 100644 --- a/tests/BecomingTest.php +++ b/tests/BecomingTest.php @@ -13,6 +13,7 @@ use Be\Framework\Exception\RuntimeSemanticVariableException; use Be\Framework\Exception\SemanticVariableException; use Be\Framework\Exception\UnbecomingException; +use Be\Framework\SemanticLog\Context\BecomingCloseContext; use Be\Framework\SemanticLog\Logger; use Be\Framework\SemanticVariable\Errors; use Be\Framework\SemanticVariable\SemanticValidator; @@ -249,17 +250,14 @@ public function testBranchingFirstStepFailsWithInputException(): void } } - public function testChainCloseLogRecordsRefinedSubtype(): void + public function testChainCloseLogRecordsBaseErrorAndInputOrigin(): void { - // The chain-close log must record the refined subtype FQCN, which fixes the - // "refine BEFORE closeChain" ordering in Becoming::__invoke. Use a real + // The chain-close log records the ORIGINAL base exception class (consistent + // with the inner being_error_close span) and carries the input/runtime + // distinction via `origin` — not by swapping the class name. Use a real // SemanticLogger so the emitted becoming_close payload can be inspected. - $injector = new Injector(new BecomingTestModule()); - $semanticValidator = new SemanticValidator('MyVendor\\MyApp\\SemanticVariables'); - $becomingArguments = new BecomingArguments($injector, $semanticValidator); $semanticLogger = new SemanticLogger(); - $logger = new Logger($semanticLogger, $becomingArguments); - $becoming = new Becoming($injector, 'MyVendor\\MyApp', $logger, $becomingArguments); + $becoming = $this->becomingWithLogger($semanticLogger); try { $becoming(new BecomingTestSemanticInvalid('invalid-email')); @@ -268,14 +266,50 @@ public function testChainCloseLogRecordsRefinedSubtype(): void // expected } + $context = $this->becomingCloseContext($semanticLogger); + $this->assertSame('error', $context['exit']); + $this->assertSame(SemanticVariableException::class, $context['error']); + $this->assertSame(BecomingCloseContext::ORIGIN_INPUT, $context['origin']); + } + + public function testChainCloseLogRecordsRuntimeOrigin(): void + { + // A validation failure on a later metamorphosis is logged with origin=runtime. + $semanticLogger = new SemanticLogger(); + $becoming = $this->becomingWithLogger($semanticLogger); + + try { + $becoming(new BecomingTestRuntimeStart('seed')); + $this->fail('Expected RuntimeSemanticVariableException'); + } catch (RuntimeSemanticVariableException) { + // expected + } + + $context = $this->becomingCloseContext($semanticLogger); + $this->assertSame('error', $context['exit']); + $this->assertSame(SemanticVariableException::class, $context['error']); + $this->assertSame(BecomingCloseContext::ORIGIN_RUNTIME, $context['origin']); + } + + private function becomingWithLogger(SemanticLogger $semanticLogger): Becoming + { + $injector = new Injector(new BecomingTestModule()); + $semanticValidator = new SemanticValidator('MyVendor\\MyApp\\SemanticVariables'); + $becomingArguments = new BecomingArguments($injector, $semanticValidator); + $logger = new Logger($semanticLogger, $becomingArguments); + + return new Becoming($injector, 'MyVendor\\MyApp', $logger, $becomingArguments); + } + + /** @return array */ + private function becomingCloseContext(SemanticLogger $semanticLogger): array + { $logData = $semanticLogger->toArray(); assert(is_array($logData['open']) && is_array($logData['open'][0]) && is_array($logData['open'][0]['close'])); $closeData = $logData['open'][0]['close']; - assert(is_array($closeData) && is_array($closeData['context'])); + assert(is_array($closeData) && $closeData['type'] === 'becoming_close' && is_array($closeData['context'])); - $this->assertSame('becoming_close', $closeData['type']); - $this->assertSame('error', $closeData['context']['exit']); - $this->assertSame(InputSemanticVariableException::class, $closeData['context']['error']); + return $closeData['context']; } public function testTypeMatchingFailureWithFallback(): void From a1f122fc10020dd87be32264b065adfad88481cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:21:08 +0000 Subject: [PATCH 4/4] Address CodeRabbit review: harden log-shape test, drop unused fixture param - becomingCloseContext(): promote the becoming_close type check from an assert() to a real PHPUnit assertion so it still holds when zend.assertions is disabled; keep the is_array() narrowing asserts that match the repo-wide logger-test convention. - BecomingTestRuntimeMiddle: use the #[Input] $value instead of a hardcoded string, removing the unused parameter (and its phpcs:ignore) at the root rather than papering over it with a suppression annotation. The seed value is itself an invalid email, so the second metamorphosis still fails. --- tests/BecomingTest.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/BecomingTest.php b/tests/BecomingTest.php index 0ebd5a61..ceedb26f 100644 --- a/tests/BecomingTest.php +++ b/tests/BecomingTest.php @@ -307,7 +307,11 @@ private function becomingCloseContext(SemanticLogger $semanticLogger): array $logData = $semanticLogger->toArray(); assert(is_array($logData['open']) && is_array($logData['open'][0]) && is_array($logData['open'][0]['close'])); $closeData = $logData['open'][0]['close']; - assert(is_array($closeData) && $closeData['type'] === 'becoming_close' && is_array($closeData['context'])); + assert(is_array($closeData) && is_array($closeData['context'])); + + // A real assertion (not assert()) so the shape check still holds when + // zend.assertions is disabled. + $this->assertSame('becoming_close', $closeData['type']); return $closeData['context']; } @@ -761,10 +765,11 @@ final class BecomingTestRuntimeMiddle public function __construct( #[Input] - string $value, // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter + string $value, ) { - // Produce an invalid email for the next metamorphosis to validate - $this->email = 'invalid-email'; + // Carry the seed forward as the email for the next step. The seed ('seed') + // is not a valid email, so the SECOND metamorphosis fails semantic validation. + $this->email = $value; } }