From 25d8ca1b53a55897c0fa5277c32100bda67bbd01 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Fri, 5 Sep 2025 18:33:59 +0300 Subject: [PATCH 1/6] Enhance AiClient::isConfigured() to support provider ID and class name input --- src/AiClient.php | 37 ++++++++- tests/unit/AiClientTest.php | 145 ++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/src/AiClient.php b/src/AiClient.php index 67bf36c8..8e55fef4 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -9,6 +9,7 @@ use WordPress\AiClient\ProviderImplementations\Google\GoogleProvider; use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; +use WordPress\AiClient\Providers\Contracts\ProviderInterface; use WordPress\AiClient\Providers\Http\HttpTransporterFactory; use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; @@ -114,14 +115,44 @@ public static function defaultRegistry(): ProviderRegistry /** * Checks if a provider is configured and available for use. * + * Supports multiple input formats for developer convenience: + * - ProviderAvailabilityInterface: Direct availability check (backward compatible) + * - string (provider ID): e.g., AiClient::isConfigured('openai') + * - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class) + * + * When using string input, this method leverages the ProviderRegistry's centralized + * dependency management, ensuring HttpTransporter and authentication are properly + * injected into availability instances. + * * @since 0.1.0 * - * @param ProviderAvailabilityInterface $availability The provider availability instance to check. + * @param ProviderAvailabilityInterface|string|class-string $availabilityOrIdOrClassName + * The provider availability instance, provider ID, or provider class name. + * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return bool True if the provider is configured and available, false otherwise. */ - public static function isConfigured(ProviderAvailabilityInterface $availability): bool + public static function isConfigured($availabilityOrIdOrClassName, ?ProviderRegistry $registry = null): bool { - return $availability->isConfigured(); + // Handle direct ProviderAvailabilityInterface (backward compatibility) + if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) { + return $availabilityOrIdOrClassName->isConfigured(); + } + + // Handle string input (provider ID or class name) via registry + if (is_string($availabilityOrIdOrClassName)) { + $registry = $registry ?? self::defaultRegistry(); + return $registry->isProviderConfigured($availabilityOrIdOrClassName); + } + + throw new \InvalidArgumentException( + 'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' . + sprintf( + 'Received: %s', + is_object($availabilityOrIdOrClassName) + ? get_class($availabilityOrIdOrClassName) + : gettype($availabilityOrIdOrClassName) + ) + ); } /** diff --git a/tests/unit/AiClientTest.php b/tests/unit/AiClientTest.php index 32f5b21b..bd7dbfd6 100644 --- a/tests/unit/AiClientTest.php +++ b/tests/unit/AiClientTest.php @@ -12,6 +12,7 @@ use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; use WordPress\AiClient\Providers\ProviderRegistry; +use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; use WordPress\AiClient\Tests\traits\MockModelCreationTrait; /** @@ -246,6 +247,150 @@ public function testIsConfiguredReturnsFalseWhenProviderIsNotConfigured(): void $this->assertFalse($result); } + /** + * Tests isConfigured method with provider ID string leverages registry. + */ + public function testIsConfiguredWithProviderIdString(): void + { + $mockRegistry = $this->createMock(ProviderRegistry::class); + $mockRegistry->expects($this->once()) + ->method('isProviderConfigured') + ->with('openai') + ->willReturn(true); + + $result = AiClient::isConfigured('openai', $mockRegistry); + + $this->assertTrue($result); + } + + /** + * Tests isConfigured method with provider class name leverages registry. + */ + public function testIsConfiguredWithProviderClassName(): void + { + $mockRegistry = $this->createMock(ProviderRegistry::class); + $mockRegistry->expects($this->once()) + ->method('isProviderConfigured') + ->with(OpenAiProvider::class) + ->willReturn(false); + + $result = AiClient::isConfigured(OpenAiProvider::class, $mockRegistry); + + $this->assertFalse($result); + } + + /** + * Tests isConfigured method with provider ID uses default registry when none provided. + */ + public function testIsConfiguredWithProviderIdUsesDefaultRegistry(): void + { + // This test will use the actual default registry since we can't easily mock static methods + // The default registry should have providers registered, so we test the delegation path + $result = AiClient::isConfigured('openai'); + + // The result will be false because no actual API keys are configured in tests, + // but the important thing is that no exception is thrown and the registry delegation works + $this->assertIsBool($result); + } + + /** + * Tests isConfigured method with provider class name uses default registry when none provided. + */ + public function testIsConfiguredWithProviderClassNameUsesDefaultRegistry(): void + { + // This test will use the actual default registry since we can't easily mock static methods + // The default registry should have providers registered, so we test the delegation path + $result = AiClient::isConfigured(OpenAiProvider::class); + + // The result will be false because no actual API keys are configured in tests, + // but the important thing is that no exception is thrown and the registry delegation works + $this->assertIsBool($result); + } + + /** + * Tests isConfigured method throws exception for invalid parameter types. + */ + public function testIsConfiguredThrowsExceptionForInvalidParameterTypes(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' . + 'Received: integer' + ); + + AiClient::isConfigured(123); + } + + /** + * Data provider for invalid isConfigured parameter types. + * + * @return array + */ + public function invalidIsConfiguredParameterTypesProvider(): array + { + return [ + 'integer parameter' => [123, 'integer'], + 'array parameter' => [['invalid_array'], 'array'], + 'object parameter' => [new \stdClass(), 'stdClass'], + 'boolean parameter' => [true, 'boolean'], + 'null parameter' => [null, 'NULL'], + ]; + } + + /** + * Tests that isConfigured rejects all invalid parameter types consistently. + * + * @dataProvider invalidIsConfiguredParameterTypesProvider + * @param mixed $invalidParam + */ + public function testIsConfiguredRejectsInvalidParameterTypes($invalidParam, string $expectedType): void + { + try { + AiClient::isConfigured($invalidParam); + $this->fail("Expected InvalidArgumentException for isConfigured with $expectedType"); + } catch (\InvalidArgumentException $e) { + $this->assertStringContainsString( + 'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name.', + $e->getMessage(), + "isConfigured should reject invalid parameter type: $expectedType" + ); + $this->assertStringContainsString( + "Received: $expectedType", + $e->getMessage(), + "isConfigured should include received type in error message" + ); + } + } + + /** + * Tests backward compatibility - isConfigured still works with ProviderAvailabilityInterface. + */ + public function testIsConfiguredBackwardCompatibility(): void + { + // Test that the original interface-based approach still works exactly as before + $mockAvailability = $this->createMock(ProviderAvailabilityInterface::class); + $mockAvailability->expects($this->once()) + ->method('isConfigured') + ->willReturn(true); + + // Should work without registry parameter + $result = AiClient::isConfigured($mockAvailability); + $this->assertTrue($result); + + // Should work with registry parameter (but registry should be ignored for interface input) + $mockRegistry = $this->createMock(ProviderRegistry::class); + $mockRegistry->expects($this->never()) + ->method('isProviderConfigured'); // Registry should not be called for interface input + + $mockAvailability2 = $this->createMock(ProviderAvailabilityInterface::class); + $mockAvailability2->expects($this->once()) + ->method('isConfigured') + ->willReturn(false); + + $result2 = AiClient::isConfigured($mockAvailability2, $mockRegistry); + $this->assertFalse($result2); + } + /** * Tests generateResult delegates to generateTextResult when model supports text generation. */ From d9a66f46a4f1f1e3324575da64551f4a3ac28302 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Fri, 5 Sep 2025 18:41:44 +0300 Subject: [PATCH 2/6] Fix code style issues - sort use statements and line length --- tests/unit/AiClientTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/AiClientTest.php b/tests/unit/AiClientTest.php index bd7dbfd6..81fb831a 100644 --- a/tests/unit/AiClientTest.php +++ b/tests/unit/AiClientTest.php @@ -9,10 +9,10 @@ use WordPress\AiClient\AiClient; use WordPress\AiClient\Messages\DTO\MessagePart; use WordPress\AiClient\Messages\DTO\UserMessage; +use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; use WordPress\AiClient\Providers\ProviderRegistry; -use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; use WordPress\AiClient\Tests\traits\MockModelCreationTrait; /** @@ -350,7 +350,8 @@ public function testIsConfiguredRejectsInvalidParameterTypes($invalidParam, stri $this->fail("Expected InvalidArgumentException for isConfigured with $expectedType"); } catch (\InvalidArgumentException $e) { $this->assertStringContainsString( - 'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name.', + 'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, ' . + 'or provider class name.', $e->getMessage(), "isConfigured should reject invalid parameter type: $expectedType" ); From bcc57f15e6385c6f78cd7ead4a3c291696be0c02 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Thu, 11 Sep 2025 10:23:46 +0300 Subject: [PATCH 3/6] Remove backward compatibility word - not needed --- src/AiClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AiClient.php b/src/AiClient.php index 8e55fef4..aaf5eabf 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -116,7 +116,7 @@ public static function defaultRegistry(): ProviderRegistry * Checks if a provider is configured and available for use. * * Supports multiple input formats for developer convenience: - * - ProviderAvailabilityInterface: Direct availability check (backward compatible) + * - ProviderAvailabilityInterface: Direct availability check * - string (provider ID): e.g., AiClient::isConfigured('openai') * - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class) * From e14a7567b10cb655dd4a90108b465d14567541e5 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Thu, 11 Sep 2025 10:29:31 +0300 Subject: [PATCH 4/6] Remove custom registry parameter from isConfigured method signature --- src/AiClient.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/AiClient.php b/src/AiClient.php index aaf5eabf..ec7c927c 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -128,10 +128,9 @@ public static function defaultRegistry(): ProviderRegistry * * @param ProviderAvailabilityInterface|string|class-string $availabilityOrIdOrClassName * The provider availability instance, provider ID, or provider class name. - * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default. * @return bool True if the provider is configured and available, false otherwise. */ - public static function isConfigured($availabilityOrIdOrClassName, ?ProviderRegistry $registry = null): bool + public static function isConfigured($availabilityOrIdOrClassName): bool { // Handle direct ProviderAvailabilityInterface (backward compatibility) if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) { @@ -140,8 +139,7 @@ public static function isConfigured($availabilityOrIdOrClassName, ?ProviderRegis // Handle string input (provider ID or class name) via registry if (is_string($availabilityOrIdOrClassName)) { - $registry = $registry ?? self::defaultRegistry(); - return $registry->isProviderConfigured($availabilityOrIdOrClassName); + return self::defaultRegistry()->isProviderConfigured($availabilityOrIdOrClassName); } throw new \InvalidArgumentException( From d71875804e4dbd45e6897bbc60211778d846664c Mon Sep 17 00:00:00 2001 From: Mohamed Khaled Date: Thu, 11 Sep 2025 10:36:10 +0300 Subject: [PATCH 5/6] Update tests for isConfigured method signature change --- tests/unit/AiClientTest.php | 40 +++++++++++++++---------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/tests/unit/AiClientTest.php b/tests/unit/AiClientTest.php index 81fb831a..c18beb27 100644 --- a/tests/unit/AiClientTest.php +++ b/tests/unit/AiClientTest.php @@ -248,35 +248,31 @@ public function testIsConfiguredReturnsFalseWhenProviderIsNotConfigured(): void } /** - * Tests isConfigured method with provider ID string leverages registry. + * Tests isConfigured method with provider ID string leverages default registry. */ public function testIsConfiguredWithProviderIdString(): void { - $mockRegistry = $this->createMock(ProviderRegistry::class); - $mockRegistry->expects($this->once()) - ->method('isProviderConfigured') - ->with('openai') - ->willReturn(true); - - $result = AiClient::isConfigured('openai', $mockRegistry); + // This test will use the actual default registry since we can't easily mock static methods + // The default registry should have providers registered, so we test the delegation path + $result = AiClient::isConfigured('openai'); - $this->assertTrue($result); + // The result will be false because no actual API keys are configured in tests, + // but the important thing is that no exception is thrown and the registry delegation works + $this->assertIsBool($result); } /** - * Tests isConfigured method with provider class name leverages registry. + * Tests isConfigured method with provider class name leverages default registry. */ public function testIsConfiguredWithProviderClassName(): void { - $mockRegistry = $this->createMock(ProviderRegistry::class); - $mockRegistry->expects($this->once()) - ->method('isProviderConfigured') - ->with(OpenAiProvider::class) - ->willReturn(false); - - $result = AiClient::isConfigured(OpenAiProvider::class, $mockRegistry); + // This test will use the actual default registry since we can't easily mock static methods + // The default registry should have providers registered, so we test the delegation path + $result = AiClient::isConfigured(OpenAiProvider::class); - $this->assertFalse($result); + // The result will be false because no actual API keys are configured in tests, + // but the important thing is that no exception is thrown and the registry delegation works + $this->assertIsBool($result); } /** @@ -378,17 +374,13 @@ public function testIsConfiguredBackwardCompatibility(): void $result = AiClient::isConfigured($mockAvailability); $this->assertTrue($result); - // Should work with registry parameter (but registry should be ignored for interface input) - $mockRegistry = $this->createMock(ProviderRegistry::class); - $mockRegistry->expects($this->never()) - ->method('isProviderConfigured'); // Registry should not be called for interface input - + // Should work in all cases with interface input $mockAvailability2 = $this->createMock(ProviderAvailabilityInterface::class); $mockAvailability2->expects($this->once()) ->method('isConfigured') ->willReturn(false); - $result2 = AiClient::isConfigured($mockAvailability2, $mockRegistry); + $result2 = AiClient::isConfigured($mockAvailability2); $this->assertFalse($result2); } From 9f6a0d98d615488f8dde3c56a222b71d2c40eac1 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 11 Sep 2025 10:16:56 -0700 Subject: [PATCH 6/6] Document parameter change and remove unnecessary tests. --- src/AiClient.php | 1 + tests/unit/AiClientTest.php | 28 ---------------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/src/AiClient.php b/src/AiClient.php index ec7c927c..3fb52c24 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -125,6 +125,7 @@ public static function defaultRegistry(): ProviderRegistry * injected into availability instances. * * @since 0.1.0 + * @since n.e.x.t Now supports being passed a provider ID or class name. * * @param ProviderAvailabilityInterface|string|class-string $availabilityOrIdOrClassName * The provider availability instance, provider ID, or provider class name. diff --git a/tests/unit/AiClientTest.php b/tests/unit/AiClientTest.php index c18beb27..dc4b3c81 100644 --- a/tests/unit/AiClientTest.php +++ b/tests/unit/AiClientTest.php @@ -275,34 +275,6 @@ public function testIsConfiguredWithProviderClassName(): void $this->assertIsBool($result); } - /** - * Tests isConfigured method with provider ID uses default registry when none provided. - */ - public function testIsConfiguredWithProviderIdUsesDefaultRegistry(): void - { - // This test will use the actual default registry since we can't easily mock static methods - // The default registry should have providers registered, so we test the delegation path - $result = AiClient::isConfigured('openai'); - - // The result will be false because no actual API keys are configured in tests, - // but the important thing is that no exception is thrown and the registry delegation works - $this->assertIsBool($result); - } - - /** - * Tests isConfigured method with provider class name uses default registry when none provided. - */ - public function testIsConfiguredWithProviderClassNameUsesDefaultRegistry(): void - { - // This test will use the actual default registry since we can't easily mock static methods - // The default registry should have providers registered, so we test the delegation path - $result = AiClient::isConfigured(OpenAiProvider::class); - - // The result will be false because no actual API keys are configured in tests, - // but the important thing is that no exception is thrown and the registry delegation works - $this->assertIsBool($result); - } - /** * Tests isConfigured method throws exception for invalid parameter types. */