Add unit test suite: 54 tests across models, core, and providers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-12 00:36:59 +07:00
parent 88d674d679
commit c6370cf854
19 changed files with 1585 additions and 478 deletions

View File

@@ -0,0 +1,114 @@
<?php
/**
* Tests for PRAutoBlogger_OpenRouter_Pricing.
*
* Validates the model pricing table and cost calculation logic.
*
* @package PRAutoBlogger\Tests\Providers
*/
namespace PRAutoBlogger\Tests\Providers;
use PRAutoBlogger\Tests\BaseTestCase;
class OpenRouterPricingTest extends BaseTestCase {
protected function setUp(): void {
parent::setUp();
require_once PRAB_PLUGIN_DIR . 'includes/providers/class-prab-openrouter-pricing.php';
}
/**
* Test that known models have pricing data.
*/
public function test_known_models_have_pricing(): void {
$known_models = [
'google/gemini-2.0-flash-001',
'anthropic/claude-3-haiku-20240307',
];
foreach ( $known_models as $model ) {
$pricing = \PRAutoBlogger_OpenRouter_Pricing::get_pricing( $model );
$this->assertNotNull(
$pricing,
"Model '{$model}' should have pricing data."
);
$this->assertArrayHasKey( 'input', $pricing );
$this->assertArrayHasKey( 'output', $pricing );
$this->assertIsFloat( $pricing['input'] );
$this->assertIsFloat( $pricing['output'] );
}
}
/**
* Test that unknown model returns null or fallback.
*/
public function test_unknown_model_returns_null(): void {
$pricing = \PRAutoBlogger_OpenRouter_Pricing::get_pricing( 'nonexistent/fake-model-999' );
$this->assertNull( $pricing );
}
/**
* Test cost calculation produces correct result.
*/
public function test_calculate_cost_basic(): void {
// Manual calculation: if input price is $X per million tokens and output is $Y per million tokens
// cost = (prompt_tokens * X + completion_tokens * Y) / 1_000_000
$cost = \PRAutoBlogger_OpenRouter_Pricing::calculate_cost(
'google/gemini-2.0-flash-001',
1000,
2000
);
$this->assertIsFloat( $cost );
$this->assertGreaterThan( 0.0, $cost );
}
/**
* Test cost calculation with zero tokens returns zero.
*/
public function test_calculate_cost_zero_tokens(): void {
$cost = \PRAutoBlogger_OpenRouter_Pricing::calculate_cost(
'google/gemini-2.0-flash-001',
0,
0
);
$this->assertSame( 0.0, $cost );
}
/**
* Test cost calculation with unknown model returns zero.
*/
public function test_calculate_cost_unknown_model_returns_zero(): void {
$cost = \PRAutoBlogger_OpenRouter_Pricing::calculate_cost(
'fake/model',
1000,
2000
);
$this->assertSame( 0.0, $cost );
}
/**
* Test that all models in the pricing table have positive prices.
*/
public function test_all_model_prices_are_positive(): void {
$all_models = \PRAutoBlogger_OpenRouter_Pricing::get_all_models();
$this->assertNotEmpty( $all_models, 'Pricing table should not be empty.' );
foreach ( $all_models as $model => $pricing ) {
$this->assertGreaterThanOrEqual(
0.0,
$pricing['input'],
"Input price for '{$model}' must be >= 0."
);
$this->assertGreaterThanOrEqual(
0.0,
$pricing['output'],
"Output price for '{$model}' must be >= 0."
);
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
/**
* Tests for PRAutoBlogger_OpenRouter_Provider.
*
* Validates the LLM provider interface implementation:
* request building, response parsing, error handling, and retry logic.
* All HTTP calls are mocked — no real API calls.
*
* @package PRAutoBlogger\Tests\Providers
*/
namespace PRAutoBlogger\Tests\Providers;
use PRAutoBlogger\Tests\BaseTestCase;
use Brain\Monkey\Functions;
class OpenRouterProviderTest extends BaseTestCase {
protected function setUp(): void {
parent::setUp();
require_once PRAB_PLUGIN_DIR . 'includes/providers/class-prab-openrouter-provider.php';
}
/**
* Test successful chat completion parses response correctly.
*/
public function test_chat_completion_parses_success_response(): void {
$mock_response = [
'response' => [
'code' => 200,
],
'body' => wp_json_encode( [
'choices' => [
[
'message' => [
'content' => 'Generated content here.',
],
],
],
'usage' => [
'prompt_tokens' => 150,
'completion_tokens' => 300,
],
] ),
];
Functions\when( 'wp_remote_post' )->justReturn( $mock_response );
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $mock_response['body'] );
Functions\when( 'is_wp_error' )->justReturn( false );
$provider = new \PRAutoBlogger_OpenRouter_Provider( 'test-api-key' );
$result = $provider->chat_completion(
'google/gemini-2.0-flash-001',
[
[ 'role' => 'user', 'content' => 'Write a short paragraph.' ],
]
);
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'content', $result );
$this->assertSame( 'Generated content here.', $result['content'] );
$this->assertArrayHasKey( 'usage', $result );
$this->assertSame( 150, $result['usage']['prompt_tokens'] );
$this->assertSame( 300, $result['usage']['completion_tokens'] );
}
/**
* Test chat completion handles WP_Error (network failure).
*/
public function test_chat_completion_handles_wp_error(): void {
$wp_error = new \stdClass();
Functions\when( 'wp_remote_post' )->justReturn( $wp_error );
Functions\when( 'is_wp_error' )->justReturn( true );
$provider = new \PRAutoBlogger_OpenRouter_Provider( 'test-api-key' );
$result = $provider->chat_completion(
'model/x',
[ [ 'role' => 'user', 'content' => 'test' ] ]
);
// Should return an error structure, not throw.
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'error', $result );
}
/**
* Test chat completion handles non-200 HTTP status.
*/
public function test_chat_completion_handles_http_error(): void {
$mock_response = [
'body' => '{"error": {"message": "Rate limit exceeded"}}',
];
Functions\when( 'wp_remote_post' )->justReturn( $mock_response );
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 429 );
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $mock_response['body'] );
Functions\when( 'is_wp_error' )->justReturn( false );
$provider = new \PRAutoBlogger_OpenRouter_Provider( 'test-api-key' );
$result = $provider->chat_completion(
'model/x',
[ [ 'role' => 'user', 'content' => 'test' ] ]
);
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'error', $result );
}
/**
* Test chat completion handles malformed JSON response.
*/
public function test_chat_completion_handles_malformed_json(): void {
Functions\when( 'wp_remote_post' )->justReturn( [ 'body' => 'not json' ] );
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
Functions\when( 'wp_remote_retrieve_body' )->justReturn( 'not json' );
Functions\when( 'is_wp_error' )->justReturn( false );
$provider = new \PRAutoBlogger_OpenRouter_Provider( 'test-api-key' );
$result = $provider->chat_completion(
'model/x',
[ [ 'role' => 'user', 'content' => 'test' ] ]
);
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'error', $result );
}
/**
* Test chat completion handles missing choices in response.
*/
public function test_chat_completion_handles_empty_choices(): void {
$body = wp_json_encode( [ 'choices' => [] ] );
Functions\when( 'wp_remote_post' )->justReturn( [ 'body' => $body ] );
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $body );
Functions\when( 'is_wp_error' )->justReturn( false );
$provider = new \PRAutoBlogger_OpenRouter_Provider( 'test-api-key' );
$result = $provider->chat_completion(
'model/x',
[ [ 'role' => 'user', 'content' => 'test' ] ]
);
$this->assertIsArray( $result );
$this->assertArrayHasKey( 'error', $result );
}
}