fix: resolve Logger access, wrong model name, and API key guard tests

- bootstrap: add PRAUTOBLOGGER_MAX_RETRIES, API_TIMEOUT, RETRY_BASE_DELAY constants
- BaseTestCase: add wp_json_encode + home_url stubs, reset Logger singleton in tearDown
- CostTrackerTest: use openai/gpt-4o (in MODEL_PRICING), fix log_api_call args,
  allow multiple wpdb inserts (Logger may also insert event_log rows)
- OpenRouterPricingTest: add wpdb mock with prefix for Logger access on unknown models
- OpenRouterProviderTest: test send_chat_completion throws RuntimeException when
  no API key is configured (matches actual guard clause behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-12 08:35:01 +07:00
parent f8b4051437
commit a5d426989c
5 changed files with 72 additions and 19 deletions

View File

@@ -29,6 +29,17 @@ if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
define( 'HOUR_IN_SECONDS', 3600 ); define( 'HOUR_IN_SECONDS', 3600 );
} }
// Plugin constants used by OpenRouter provider retry logic.
if ( ! defined( 'PRAUTOBLOGGER_MAX_RETRIES' ) ) {
define( 'PRAUTOBLOGGER_MAX_RETRIES', 3 );
}
if ( ! defined( 'PRAUTOBLOGGER_API_TIMEOUT_SECONDS' ) ) {
define( 'PRAUTOBLOGGER_API_TIMEOUT_SECONDS', 30 );
}
if ( ! defined( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS' ) ) {
define( 'PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS', 1 );
}
// WordPress database constants used in $wpdb queries. // WordPress database constants used in $wpdb queries.
if ( ! defined( 'ARRAY_A' ) ) { if ( ! defined( 'ARRAY_A' ) ) {
define( 'ARRAY_A', 'ARRAY_A' ); define( 'ARRAY_A', 'ARRAY_A' );

View File

@@ -32,12 +32,27 @@ abstract class BaseTestCase extends TestCase {
Functions\when( 'get_transient' )->justReturn( false ); Functions\when( 'get_transient' )->justReturn( false );
Functions\when( 'set_transient' )->justReturn( true ); Functions\when( 'set_transient' )->justReturn( true );
Functions\when( 'delete_transient' )->justReturn( true ); Functions\when( 'delete_transient' )->justReturn( true );
// wp_json_encode is used by Logger and other classes for meta serialisation.
Functions\when( 'wp_json_encode' )->alias( 'json_encode' );
// home_url is used by OpenRouter provider in request headers.
Functions\when( 'home_url' )->justReturn( 'https://test.example.com' );
} }
/** /**
* Tear down Brain\Monkey after each test. * Tear down Brain\Monkey after each test.
*/ */
protected function tearDown(): void { protected function tearDown(): void {
// Reset Logger singleton to prevent state leakage between tests.
// Logger caches its log-level threshold and holds a static instance.
if ( class_exists( 'PRAutoBlogger_Logger', false ) ) {
$ref = new \ReflectionClass( \PRAutoBlogger_Logger::class );
$prop = $ref->getProperty( 'instance' );
$prop->setAccessible( true );
$prop->setValue( null, null );
}
Monkey\tearDown(); Monkey\tearDown();
parent::tearDown(); parent::tearDown();
} }

View File

@@ -142,23 +142,23 @@ class CostTrackerTest extends BaseTestCase {
$tracker = new \PRAutoBlogger_Cost_Tracker(); $tracker = new \PRAutoBlogger_Cost_Tracker();
$tracker->set_run_id( 'run_test' ); $tracker->set_run_id( 'run_test' );
// Mock wpdb to expect an insert call. // Allow multiple inserts — log_api_call inserts a generation log row,
$this->wpdb->expects( $this->once() ) // and Logger may also insert an event log row if a warning fires.
$this->wpdb->expects( $this->atLeastOnce() )
->method( 'insert' ) ->method( 'insert' )
->willReturn( 1 ); ->willReturn( 1 );
// Signature: log_api_call(?int $post_id, string $stage, string $provider, // Actual signature: log_api_call(?int $post_id, string $stage,
// string $model, int $prompt_tokens, int $completion_tokens, // string $provider, string $model, int $prompt_tokens,
// float $estimated_cost, float $duration_seconds). // int $completion_tokens, string $response_status, string $error_message).
// Use 'openai/gpt-4o' which IS in MODEL_PRICING to avoid unknown-model warnings.
$tracker->log_api_call( $tracker->log_api_call(
123, 123,
'analysis', 'analysis',
'openrouter', 'openrouter',
'openai/gpt-4', 'openai/gpt-4o',
500, 500,
250, 250
0.01,
1.5
); );
// If we get here without exception, the method worked. // If we get here without exception, the method worked.

View File

@@ -14,9 +14,28 @@ use Brain\Monkey\Functions;
class OpenRouterPricingTest extends BaseTestCase { class OpenRouterPricingTest extends BaseTestCase {
/**
* @var \PHPUnit\Framework\MockObject\MockObject Mock $wpdb for Logger.
*/
private $wpdb;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
// Logger (called on unknown-model warning) accesses $wpdb->prefix and $wpdb->insert.
$this->wpdb = $this->create_mock_wpdb();
$this->wpdb->method( 'insert' )->willReturn( 1 );
$GLOBALS['wpdb'] = $this->wpdb;
// Stub get_option for Logger's log_level and Pricing's API key lookup.
$this->stub_get_option( [
'prautoblogger_log_level' => 'info',
'prautoblogger_openrouter_api_key' => '',
] );
// Stub current_time — used by Logger when writing log entries.
$this->stub_current_time( '2026-04-12 10:00:00' );
// OpenRouterPricing::get_available_models() calls get_transient() to check cache. // OpenRouterPricing::get_available_models() calls get_transient() to check cache.
// Return a valid cached model list so it doesn't try to make HTTP calls. // Return a valid cached model list so it doesn't try to make HTTP calls.
Functions\when( 'get_transient' )->alias( Functions\when( 'get_transient' )->alias(
@@ -44,6 +63,11 @@ class OpenRouterPricingTest extends BaseTestCase {
Functions\when( 'set_transient' )->justReturn( true ); Functions\when( 'set_transient' )->justReturn( true );
} }
protected function tearDown(): void {
unset( $GLOBALS['wpdb'] );
parent::tearDown();
}
/** /**
* Test Pricing can be instantiated. * Test Pricing can be instantiated.
*/ */

View File

@@ -21,8 +21,10 @@ class OpenRouterProviderTest extends BaseTestCase {
// Stub get_option — provider calls get_option('prautoblogger_openrouter_api_key', '') // Stub get_option — provider calls get_option('prautoblogger_openrouter_api_key', '')
// for API key retrieval via private get_api_key() method. // for API key retrieval via private get_api_key() method.
// Return empty string so decryption is skipped (no key = empty return). // Return empty string so decryption is skipped (no key = empty return).
// Also include prautoblogger_log_level for Logger singleton.
$this->stub_get_option( [ $this->stub_get_option( [
'prautoblogger_openrouter_api_key' => '', 'prautoblogger_openrouter_api_key' => '',
'prautoblogger_log_level' => 'info',
] ); ] );
// Stub wp_salt — called by PRAutoBlogger_Encryption during API key decryption. // Stub wp_salt — called by PRAutoBlogger_Encryption during API key decryption.
@@ -131,24 +133,26 @@ class OpenRouterProviderTest extends BaseTestCase {
} }
/** /**
* Test send_chat_completion returns array. * Test send_chat_completion throws RuntimeException when API key is empty.
*
* The guard clause at the top of send_chat_completion checks for a
* configured API key before making any HTTP calls.
*/ */
public function test_send_chat_completion_returns_array(): void { public function test_send_chat_completion_throws_without_api_key(): void {
$provider = new \PRAutoBlogger_OpenRouter_Provider(); $provider = new \PRAutoBlogger_OpenRouter_Provider();
$messages = [ $messages = [
[ 'role' => 'user', 'content' => 'Test message' ], [ 'role' => 'user', 'content' => 'Test message' ],
]; ];
$result = $provider->send_chat_completion( $messages, 'model/test', [] ); $this->expectException( \RuntimeException::class );
$provider->send_chat_completion( $messages, 'model/test', [] );
$this->assertIsArray( $result );
} }
/** /**
* Test send_chat_completion with options. * Test send_chat_completion with options also throws when no key.
*/ */
public function test_send_chat_completion_with_options(): void { public function test_send_chat_completion_with_options_throws_without_api_key(): void {
$provider = new \PRAutoBlogger_OpenRouter_Provider(); $provider = new \PRAutoBlogger_OpenRouter_Provider();
$messages = [ $messages = [
@@ -156,8 +160,7 @@ class OpenRouterProviderTest extends BaseTestCase {
]; ];
$options = [ 'temperature' => 0.7 ]; $options = [ 'temperature' => 0.7 ];
$result = $provider->send_chat_completion( $messages, 'model/test', $options ); $this->expectException( \RuntimeException::class );
$provider->send_chat_completion( $messages, 'model/test', $options );
$this->assertIsArray( $result );
} }
} }