Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/AiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't purely for backward compatibility, we just want to support all 3 ways.

Suggested change
* - ProviderAvailabilityInterface: Direct availability check (backward compatible)
* - ProviderAvailabilityInterface: Direct availability check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved e14a756

* - 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<ProviderInterface> $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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't allow passing a custom registry, that's unrelated. The AiClient for now should always operate with the defaultRegistry().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you. resolved e14a756

{
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)
)
);
}

/**
Expand Down
146 changes: 146 additions & 0 deletions tests/unit/AiClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -246,6 +247,151 @@ 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<string, array{mixed, string}>
*/
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.
*/
Expand Down