php-agentic/tests/Unit/OpenAIServiceTest.php

400 lines
13 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Tests\Unit;
use Core\Mod\Agentic\Services\AgenticResponse;
use Core\Mod\Agentic\Services\OpenAIService;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Tests\TestCase;
/**
* Tests for the OpenAIService AI provider.
*
* Uses mocked HTTP responses to test the service without real API calls.
*/
class OpenAIServiceTest extends TestCase
{
private const API_URL = 'https://api.openai.com/v1/chat/completions';
// =========================================================================
// Service Configuration Tests
// =========================================================================
public function test_name_returns_openai(): void
{
$service = new OpenAIService('test-api-key');
$this->assertEquals('openai', $service->name());
}
public function test_default_model_returns_configured_model(): void
{
$service = new OpenAIService('test-api-key', 'gpt-4o');
$this->assertEquals('gpt-4o', $service->defaultModel());
}
public function test_default_model_uses_gpt4o_mini_when_not_specified(): void
{
$service = new OpenAIService('test-api-key');
$this->assertEquals('gpt-4o-mini', $service->defaultModel());
}
public function test_is_available_returns_true_with_api_key(): void
{
$service = new OpenAIService('test-api-key');
$this->assertTrue($service->isAvailable());
}
public function test_is_available_returns_false_without_api_key(): void
{
$service = new OpenAIService('');
$this->assertFalse($service->isAvailable());
}
// =========================================================================
// Generate Tests
// =========================================================================
public function test_generate_returns_agentic_response(): void
{
Http::fake([
self::API_URL => Http::response([
'id' => 'chatcmpl-123',
'object' => 'chat.completion',
'model' => 'gpt-4o-mini',
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello, world!',
],
'finish_reason' => 'stop',
],
],
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 5,
'total_tokens' => 15,
],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('You are helpful.', 'Say hello');
$this->assertInstanceOf(AgenticResponse::class, $response);
$this->assertEquals('Hello, world!', $response->content);
$this->assertEquals('gpt-4o-mini', $response->model);
$this->assertEquals(10, $response->inputTokens);
$this->assertEquals(5, $response->outputTokens);
$this->assertEquals('stop', $response->stopReason);
}
public function test_generate_sends_correct_request_body(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$service->generate('System prompt', 'User prompt');
Http::assertSent(function ($request) {
$body = $request->data();
return $body['messages'][0]['role'] === 'system'
&& $body['messages'][0]['content'] === 'System prompt'
&& $body['messages'][1]['role'] === 'user'
&& $body['messages'][1]['content'] === 'User prompt'
&& $body['model'] === 'gpt-4o-mini'
&& $body['max_tokens'] === 4096
&& $body['temperature'] === 1.0;
});
}
public function test_generate_uses_custom_config(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$service->generate('System', 'User', [
'model' => 'gpt-4o',
'max_tokens' => 8192,
'temperature' => 0.5,
]);
Http::assertSent(function ($request) {
$body = $request->data();
return $body['model'] === 'gpt-4o'
&& $body['max_tokens'] === 8192
&& $body['temperature'] === 0.5;
});
}
public function test_generate_sends_correct_headers(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key-123');
$service->generate('System', 'User');
Http::assertSent(function ($request) {
return $request->hasHeader('Authorization', 'Bearer test-api-key-123')
&& $request->hasHeader('Content-Type', 'application/json');
});
}
public function test_generate_tracks_duration(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertIsInt($response->durationMs);
$this->assertGreaterThanOrEqual(0, $response->durationMs);
}
public function test_generate_includes_raw_response(): void
{
$rawResponse = [
'id' => 'chatcmpl-123',
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
];
Http::fake([
self::API_URL => Http::response($rawResponse, 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals('chatcmpl-123', $response->raw['id']);
}
// =========================================================================
// Error Handling Tests
// =========================================================================
public function test_generate_throws_on_client_error(): void
{
Http::fake([
self::API_URL => Http::response([
'error' => ['message' => 'Invalid API key'],
], 401),
]);
$service = new OpenAIService('invalid-key');
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('OpenAI API error');
$service->generate('System', 'User');
}
public function test_generate_retries_on_rate_limit(): void
{
Http::fake([
self::API_URL => Http::sequence()
->push(['error' => ['message' => 'Rate limited']], 429)
->push([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Success after retry'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals('Success after retry', $response->content);
}
public function test_generate_retries_on_server_error(): void
{
Http::fake([
self::API_URL => Http::sequence()
->push(['error' => ['message' => 'Server error']], 500)
->push([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Success after retry'], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals('Success after retry', $response->content);
}
public function test_generate_throws_after_max_retries(): void
{
Http::fake([
self::API_URL => Http::response(['error' => ['message' => 'Server error']], 500),
]);
$service = new OpenAIService('test-api-key');
$this->expectException(RuntimeException::class);
$service->generate('System', 'User');
}
// =========================================================================
// Stream Tests
// =========================================================================
public function test_stream_is_generator(): void
{
// Create a simple SSE stream response
$stream = "data: {\"choices\": [{\"delta\": {\"content\": \"Hello\"}}]}\n\n";
$stream .= "data: {\"choices\": [{\"delta\": {\"content\": \" world\"}}]}\n\n";
$stream .= "data: [DONE]\n\n";
Http::fake([
self::API_URL => Http::response($stream, 200, ['Content-Type' => 'text/event-stream']),
]);
$service = new OpenAIService('test-api-key');
$generator = $service->stream('System', 'User');
$this->assertInstanceOf(\Generator::class, $generator);
}
public function test_stream_sends_stream_flag(): void
{
Http::fake([
self::API_URL => Http::response('', 200),
]);
$service = new OpenAIService('test-api-key');
iterator_to_array($service->stream('System', 'User'));
Http::assertSent(function ($request) {
return $request->data()['stream'] === true;
});
}
// =========================================================================
// Response Handling Edge Cases
// =========================================================================
public function test_handles_empty_choices(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 0],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals('', $response->content);
}
public function test_handles_missing_usage_data(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response'], 'finish_reason' => 'stop'],
],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals(0, $response->inputTokens);
$this->assertEquals(0, $response->outputTokens);
}
public function test_handles_missing_finish_reason(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => 'Response']],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 5],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertNull($response->stopReason);
}
public function test_handles_null_content(): void
{
Http::fake([
self::API_URL => Http::response([
'model' => 'gpt-4o-mini',
'choices' => [
['message' => ['content' => null], 'finish_reason' => 'stop'],
],
'usage' => ['prompt_tokens' => 10, 'completion_tokens' => 0],
], 200),
]);
$service = new OpenAIService('test-api-key');
$response = $service->generate('System', 'User');
$this->assertEquals('', $response->content);
}
}